UNPKG

@godspeedsystems/godspeed

Version:

Godspeed CLI

527 lines 24.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.genGraphqlSchema = exports.generateProjectFromDotGodspeed = exports.installPackage = exports.installDependencies = exports.compileAndCopyOrJustCopy = exports.generateFromExamples = exports.cloneProjectTemplate = exports.copyingLocalTemplate = exports.readDotGodspeed = exports.validateAndCreateProjectDirectory = void 0; const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const os_1 = __importDefault(require("os")); const yaml_1 = __importDefault(require("yaml")); const inquirer_1 = __importDefault(require("inquirer")); const signale_1 = require("./signale"); const glob_1 = require("glob"); const ejs_1 = __importDefault(require("ejs")); const simple_git_1 = __importDefault(require("simple-git")); const chalk_1 = __importDefault(require("chalk")); const child_process_1 = require("child_process"); const cross_spawn_1 = __importDefault(require("cross-spawn")); const ora_1 = __importDefault(require("ora")); const core_1 = require("@godspeedsystems/core"); const { exec } = require("child_process"); const userID = () => { if (process.platform == "linux") { const cmd = (0, child_process_1.spawnSync)("id", ["-u"]); const uid = cmd.stdout?.toString().trim(); return uid; } else { return "1000"; } }; const validateAndCreateProjectDirectory = async (projectDirPath) => { try { const isDirExist = fs_extra_1.default.existsSync(projectDirPath); if (isDirExist) { const answers = await inquirer_1.default.prompt([ { type: "confirm", name: "overwrite", message: () => { console.log(`${chalk_1.default.yellow(projectDirPath)} already exists.\n`); return chalk_1.default.red("Do you want to overwrite the project folder?"); }, default: false, }, ]); if (!answers.overwrite) { console.log(chalk_1.default.red("\nExiting godspeed create without creating project.")); process.exit(0); } fs_extra_1.default.rmSync(projectDirPath, { recursive: true, force: true }); } fs_extra_1.default.mkdirSync(projectDirPath); } catch (error) { console.log(chalk_1.default.red("Error while validateAndCreateProjectDirectory.", error)); } }; exports.validateAndCreateProjectDirectory = validateAndCreateProjectDirectory; const readDotGodspeed = async (projectDirPath) => { let godspeedOptions = JSON.parse(fs_extra_1.default.readFileSync(path_1.default.resolve(projectDirPath, ".godspeed"), "utf-8")); return godspeedOptions; }; exports.readDotGodspeed = readDotGodspeed; const copyingLocalTemplate = async (projectDirPath, templateDir) => { if (!fs_extra_1.default.lstatSync(templateDir)) { signale_1.log.fatal(`${chalk_1.default.red(templateDir)} does not exist or path is incorrect.`); process.exit(1); } signale_1.log.wait(`Copying template from ${chalk_1.default.yellow(templateDir)}`); fs_extra_1.default.cpSync(templateDir, projectDirPath, { recursive: true }); signale_1.log.success("Copying template successful."); }; exports.copyingLocalTemplate = copyingLocalTemplate; const cloneProjectTemplate = async (projectDirPath) => { try { signale_1.log.wait(`Cloning project template.`); const git = (0, simple_git_1.default)(); const REPO = `${process.env.GITHUB_REPO_URL}`; // clone godspeedsystems/godspeed-scaffolding repo await git.clone(REPO, projectDirPath, { "--branch": `${process.env.GITHUB_REPO_BRANCH}`, "--depth": "1", }); // TODO: remove git remote signale_1.log.success("Cloning template successful."); } catch (error) { signale_1.log.fatal(`Not able to reach template repository.`); } }; exports.cloneProjectTemplate = cloneProjectTemplate; const generateFromExamples = async (projectDirPath, exampleName = "hello-world") => { signale_1.log.wait(`Generating project with ${chalk_1.default.yellow(exampleName === "hello-world" ? "default" : exampleName)} examples.`); if (!fs_extra_1.default.existsSync(path_1.default.resolve(projectDirPath, ".template", "examples", exampleName))) { signale_1.log.fatal(`${chalk_1.default.red(exampleName)} is not a valid example.`); process.exit(0); } fs_extra_1.default.cpSync(path_1.default.resolve(projectDirPath, ".template", "examples", exampleName), path_1.default.resolve(projectDirPath), { recursive: true, }); // read if there is an .godspeed file if (fs_extra_1.default.existsSync(path_1.default.resolve(projectDirPath, ".template", "examples", exampleName, ".godspeed"))) { return await (0, exports.readDotGodspeed)(projectDirPath); } else { return null; } }; exports.generateFromExamples = generateFromExamples; const compileAndCopyOrJustCopy = async (projectDirPath, sourceFolder, destinationFolder, templateData) => { try { const fileList = (0, glob_1.globSync)(path_1.default.resolve(projectDirPath, sourceFolder + "/**/*")); let isUpdateCall = false; try { isUpdateCall = fs_extra_1.default .lstatSync(path_1.default.resolve(process.cwd(), ".godspeed")) .isFile(); } catch (error) { } fileList.map(async (sourceFilePath) => { if (fs_extra_1.default.lstatSync(sourceFilePath).isFile()) { // CREATE = used from outside // UPDATE = used from the directory itself let relativeDestinationPath; relativeDestinationPath = !isUpdateCall ? path_1.default.relative(path_1.default.resolve(projectDirPath, sourceFolder), sourceFilePath) : path_1.default.resolve(projectDirPath, destinationFolder, path_1.default.relative(path_1.default.resolve(projectDirPath, sourceFolder), sourceFilePath)); let finalDestinationWithFileName = path_1.default.resolve(projectDirPath, destinationFolder, relativeDestinationPath); const finallFolderName = path_1.default.dirname(finalDestinationWithFileName); const finalFileName = finalDestinationWithFileName.substring(finalDestinationWithFileName.lastIndexOf(path_1.default.sep) + 1); const fileName = sourceFilePath.split(path_1.default.sep).pop() || ""; const isTemplate = fileName.endsWith(".ejs"); if (isTemplate) { // compile and save const template = await ejs_1.default.compile(fs_extra_1.default.readFileSync(sourceFilePath, "utf-8")); const finalRender = await template(templateData); if (!fs_extra_1.default.existsSync(finallFolderName)) { await fs_extra_1.default.mkdirSync(finallFolderName, { recursive: true, }); } if (finallFolderName && finalFileName) { await fs_extra_1.default.writeFileSync(path_1.default.resolve(finallFolderName, fileName.replace(".ejs", "")), finalRender.replace(/^\s*\n/gm, "")); } } else { // just copy if (fileName) { try { await fs_extra_1.default.cpSync(sourceFilePath, path_1.default.resolve(finallFolderName, finalFileName), { recursive: true, }); } catch (error) { throw error; } } } } }); } catch (error) { throw error; } }; exports.compileAndCopyOrJustCopy = compileAndCopyOrJustCopy; const installDependencies = async (projectDirPath, projectName) => { const spinner = (0, ora_1.default)({ 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}...`; spinner.stop(); const installArgs = packageManager === "pnpm" ? ["install"] : ["install", "--prefer-offline"]; await new Promise((resolve, reject) => { const child = (0, cross_spawn_1.default)(packageManager, installArgs, { cwd: projectDirPath, stdio: "inherit", // Changed from "pipe" to "inherit" to show output }); child.on("close", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Process exited with code ${code}`)); } }); child.on("error", (err) => { reject(err); }); }); console.log(`${chalk_1.default.green("\nSuccessfully created the project")} ${chalk_1.default.yellow(projectName)}.`); console.log(`${chalk_1.default.green("Use `godspeed help` command for available commands.")} ${chalk_1.default.green.bold("\n\nHappy building microservices with Godspeed! 🚀🎉\n")}`); } catch (error) { console.error("Error during installation:", error.message); } }; exports.installDependencies = installDependencies; // Check if a command exists and is executable async function checkCommandExists(command) { 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) => { resolve(!error); }); }); } catch (error) { return false; } } // Enable corepack and prepare pnpm async function enableCorepackAndPnpm() { try { // Enable corepack await new Promise((resolve, reject) => { const child = (0, cross_spawn_1.default)("corepack", ["enable"], { stdio: "pipe" }); 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); }); }); // Prepare and activate pnpm await new Promise((resolve, reject) => { const child = (0, cross_spawn_1.default)("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"); } } const installPackage = async (projectDirPath, package_name) => { async function installprisma() { const command = `npm install ${package_name}`; return new Promise((resolve, reject) => { const child = exec(command, { cwd: projectDirPath, stdio: "inherit", // Redirect output }); child.on("exit", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Command exited with non-zero status code: ${code}`)); } }); child.on("error", (error) => { reject(error); }); }); } await installprisma(); }; exports.installPackage = installPackage; const generateProjectFromDotGodspeed = async (projectName, projectDirPath, godspeedOptions, exampleName, isUpdate) => { try { signale_1.log.wait("Generating project files."); const { gsNodeServiceVersion, servicePort, mongodb, postgresql, mysql, kafka, redis, elasticsearch, } = godspeedOptions; // fetch UID information let userUID = userID(); // generate .godspeed file await fs_extra_1.default.writeFileSync(path_1.default.resolve(projectDirPath, ".godspeed"), JSON.stringify(godspeedOptions, null, 2)); // generate all the dot config files if (!isUpdate) { await fs_extra_1.default.cpSync(path_1.default.resolve(projectDirPath, ".template", "dot-configs"), path_1.default.resolve(projectDirPath), { recursive: true }); // generate package.json, tsConfig.json for (let file of ["package.json", "tsconfig.json"]) { const packageJson = await fs_extra_1.default.readJson(path_1.default.resolve(projectDirPath, `.template/${file}`)); await fs_extra_1.default.writeJsonSync(path_1.default.resolve(projectDirPath, file), { ...packageJson, name: projectName, }, { spaces: "\t", }); } // generate .swcrc file const swcrc = await fs_extra_1.default.readJson(path_1.default.resolve(projectDirPath, ".template/dot-configs/.swcrc")); await fs_extra_1.default.writeJsonSync(path_1.default.resolve(projectDirPath, ".swcrc"), { ...swcrc, }, { spaces: "\t", }); // create folder structure if (exampleName) { fs_extra_1.default.cpSync(path_1.default.resolve(projectDirPath, ".template", "examples", exampleName), path_1.default.resolve(projectDirPath), { recursive: true, }); } else { fs_extra_1.default.cpSync(path_1.default.resolve(projectDirPath, ".template", "defaults"), path_1.default.resolve(projectDirPath), { recursive: true, }); } // create project folder structure // const projectStructure = [ // "config", // "src/events", // "src/functions", // "src/datasources", // "src/mappings", // ]; // projectStructure.map(async (folderName) => { // await fsExtras.mkdirSync(path.resolve(projectDirPath, folderName), { // recursive: true, // }); // }); } // TODO: generate helm-chats // fsExtras.cpSync( // path.resolve(projectDirPath, ".template/helm-charts"), // path.resolve(projectDirPath), // { recursive: true } // ); /** * Let's write * <><><><><><> * What are the files for the project * .godspeed [Done] * .vscode/* [Done] * .devcontainer/* [Done] * /helm-charts * package.json [Done] * /config [Done] * /dot-config-files => .dockerignore, .env .eslintrc.json .gitignore .prettierrc .prismagenerator [Done] * /src [Done] * /datasources [Done] * /events [Done] * /functions [Done] * /mappings [Done] */ await (0, exports.compileAndCopyOrJustCopy)(projectDirPath, ".template/.devcontainer", ".devcontainer", { dockerRegistry: process.env.DOCKER_REGISTRY, dockerPackageName: process.env.DOCKER_PACKAGE_NAME, tag: gsNodeServiceVersion, projectName: projectName, servicePort, userUID, mongodb, postgresql, mysql, kafka, redis, elasticsearch, }); signale_1.log.success("Successfully generated godspeed project files.\n"); } catch (error) { signale_1.log.fatal("Error while generating files.", error); } }; exports.generateProjectFromDotGodspeed = generateProjectFromDotGodspeed; const genGraphqlSchema = async () => { try { const availableApoloeventsources = (0, glob_1.globSync)(path_1.default.join(process.cwd(), "src/eventsources/*.yaml").replace(/\\/g, "/")); // Filter files that contain 'Apollo' in their name const apolloEventsources = availableApoloeventsources.map((file) => path_1.default.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:", choices: apolloEventsources, }, ]; async function runPrompt() { try { const answers = await inquirer_1.default.prompt(questions); if (answers.selectedOptions.length == 0) { console.log(chalk_1.default.red("Please select atleast one GraphQL eventsource")); } else { await createSwaggerFile(answers.selectedOptions); } } catch (error) { console.error(error); } } runPrompt(); } catch (error) { console.log(error); } const createSwaggerFile = async (eventSources) => { const eventPath = path_1.default.join(process.cwd(), "/src/events"); const definitionsPath = path_1.default.join(process.cwd(), "/src/definitions"); const allEventsSchema = await (0, core_1.yamlLoader)(eventPath, true); //all events of the project const definitions = await (0, core_1.yamlLoader)(definitionsPath, false); eventSources.map(async (eventSourceName) => { core_1.logger.info("Generating graphql schema for %s. First we will create swagger schema in %s", eventSourceName, os_1.default.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. // It could be one like 'http' or more like 'http & graphql' if (eventSourceKey == eventSourceName) { return true; } const eventSources = eventSourceKey.split("&").map((s) => s.trim()); return eventSources.includes(eventSourceName); })); if (Object.keys(eventSchemas).length === 0) { core_1.logger.fatal(chalk_1.default.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_1.default.parse(fs_extra_1.default.readFileSync(process.cwd() + `/src/eventsources/${eventSourceName}.yaml`, { encoding: "utf-8" })); //The yaml file of the eventsource let swaggerSchema = (0, core_1.generateSwaggerJSON)(eventSchemas, definitions, eventSourceConfig); // For swagger-to-graphql plugin we need to save this file somewhere const swaggerFilePath = path_1.default.join(os_1.default.tmpdir(), eventSourceName + "-swagger.json"); // Write the swagger.json file in the temp folder await fs_extra_1.default.writeFileSync(swaggerFilePath, JSON.stringify(swaggerSchema, null, 2)); core_1.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); }); }; }; exports.genGraphqlSchema = genGraphqlSchema; async function generateGraphqlSchema(eventSourceName, swaggerFilePath) { const command = `npx swagger-to-graphql --swagger-schema=${swaggerFilePath} > ./src/eventsources/${eventSourceName}.graphql`; exec(command, (error, stdout, stderr) => { if (error) { console.log(chalk_1.default.red.bold(`Failed to generate Graphql schema for eventsource ${eventSourceName}`)); console.log(chalk_1.default.red(error.message)); return; } if (stderr) { console.error(`stderr: ${stderr}`); return; } console.log(chalk_1.default.green(`Graphql schema generated successfuly for eventsource ${eventSourceName} at ./src/eventsources/${eventSourceName}.graphql`)); }); } const generateSwaggerui = async (eventsSchema, definitions) => { let finalSpec = {}; 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", }, license: { name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html", }, }, paths: {}, }; let swaggerSpecBase = JSON.parse(JSON.stringify(swaggerCommonPart)); finalSpec = swaggerSpecBase; Object.keys(eventsSchema).forEach((event) => { 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 let methodSpec = { summary: eventSchema.summary, description: eventSchema.description, requestBody: eventSchema.body || eventSchema.data?.schema?.body, parameters: eventSchema.parameters || eventSchema.params || eventSchema.data?.schema?.params, responses: eventSchema.responses, }; // Set it in the overall schema finalSpec.paths[apiEndPoint] = { ...finalSpec.paths[apiEndPoint], [method]: methodSpec, }; }); finalSpec.definitions = definitions; return finalSpec; }; //# sourceMappingURL=index.js.map