UNPKG

genezio

Version:

Command line utility to interact with Genezio infrastructure.

775 lines (774 loc) 38.9 kB
import { existsSync, promises as fs } from "fs"; import path from "path"; import { debugLogger, log } from "../../utils/logging.js"; import { isExpressBackend, isFastifyBackend, isNextjsComponent, isNitroComponent, isNuxtComponent, isReactComponent, isViteComponent, isServerlessHttpBackend, isVueComponent, isAngularComponent, isSvelteComponent, isFlaskComponent, isDjangoComponent, isFastAPIComponent, isPythonLambdaFunction, findEntryFile, isGenezioTypesafe, hasPostgresDependency, hasMongoDependency, isNestjsComponent, isRemixComponent, isEmberComponent, isStreamlitComponent, isTypescript, } from "./frameworks.js"; import { generateDatabaseName, readOrAskConfig } from "../deploy/utils.js"; import { NODE_DEFAULT_PACKAGE_MANAGER, packageManagers, PYTHON_DEFAULT_PACKAGE_MANAGER, } from "../../packageManagers/packageManager.js"; import { DEFAULT_NODE_RUNTIME, DEFAULT_PYTHON_RUNTIME, SSRFrameworkComponentType, } from "../../models/projectOptions.js"; import { addBackendComponentToConfig, addFrontendComponentToConfig, addServicesToConfig, addSSRComponentToConfig, getEntryFileTypescript, getPythonHandler, handleBackendAndSSRConfig, injectBackendUrlsInConfig, injectSDKInConfig, } from "./utils.js"; import { DatabaseType, FunctionType, Language } from "../../projectConfiguration/yaml/models.js"; import { report } from "./outputUtils.js"; import { isCI } from "../../utils/process.js"; import { DJANGO_PATTERN, EXPRESS_PATTERN, FASTAPI_PATTERN, FASTIFY_PATTERN, FLASK_PATTERN, PYTHON_LAMBDA_PATTERN, SERVERLESS_HTTP_PATTERN, STREAMLIT_PATTERN, } from "./constants.js"; import { analyzeEnvironmentVariableExampleFile } from "./agent.js"; export var FRONTEND_ENV_PREFIX; (function (FRONTEND_ENV_PREFIX) { FRONTEND_ENV_PREFIX["React"] = "REACT_APP"; FRONTEND_ENV_PREFIX["Vite"] = "VITE"; FRONTEND_ENV_PREFIX["Vue"] = "VUE_APP"; })(FRONTEND_ENV_PREFIX || (FRONTEND_ENV_PREFIX = {})); export var SUPPORTED_FORMATS; (function (SUPPORTED_FORMATS) { SUPPORTED_FORMATS["JSON"] = "json"; SUPPORTED_FORMATS["LIST"] = "list"; SUPPORTED_FORMATS["MARKDOWN"] = "markdown"; SUPPORTED_FORMATS["TEXT"] = "text"; })(SUPPORTED_FORMATS || (SUPPORTED_FORMATS = {})); export const DEFAULT_FORMAT = SUPPORTED_FORMATS.TEXT; export const DEFAULT_CI_FORMAT = SUPPORTED_FORMATS.JSON; export const KEY_DEPENDENCY_FILES = ["package.json", "requirements.txt", "pyproject.toml"]; export const ENVIRONMENT_EXAMPLE_FILES = [ ".env.template", ".env.example", ".env.local.example", ".env.sample", ]; export const KEY_FILES = [...KEY_DEPENDENCY_FILES, ...ENVIRONMENT_EXAMPLE_FILES]; export const EXCLUDED_DIRECTORIES = [ "node_modules", ".git", "dist", "build", "tests", ".next", ".nuxt", ".opennext", ".vercel", ".netlify", ]; export const NODE_DEFAULT_ENTRY_FILE = "index.mjs"; export const PYTHON_DEFAULT_ENTRY_FILE = "app.py"; // The analyze command has 2 side effects: // 1. It creates a new yaml with the detected components // 2. Reports the detected components to stdout export async function analyzeCommand(options) { const frameworksDetected = {}; const configPath = options.config; const rootDirectory = process.cwd(); const format = isCI() ? options.format || DEFAULT_CI_FORMAT : options.format || DEFAULT_FORMAT; if (!options.force && existsSync(configPath)) { debugLogger.debug(`Configuration file found at ${configPath}. Skipping analysis.`); if (options.format === SUPPORTED_FORMATS.TEXT) { log.info(`Configuration file found at ${configPath}. Skipping analysis. To overwrite the configuration file, run \`genezio analyze --force\` flag.`); } return; } // Create a configuration object to add components to let genezioConfig = (await readOrAskConfig(configPath, options.name, options.region)); // Search the key files in the root directory and return a map of filenames and relative paths const componentFiles = await findKeyFiles(rootDirectory); // Early return and let the user write the configs, from our perspective seems like there's nothing to be deployed if (componentFiles.size === 0) { frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend?.push({ component: "other", }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "other", }); const result = report(format, frameworksDetected, {}); if (options.format === SUPPORTED_FORMATS.TEXT) { log.info(`We didn't detect a supported stack in your project. If you need assistance or would like to request support for additional frameworks, please reach out to us at support.`); } log.info(result); return; } debugLogger.debug("Key component files found:", componentFiles); // The order of the components matters - the first one found will be added to the config const dependenciesFiles = new Map(Array.from(componentFiles).filter(([_, filename]) => KEY_DEPENDENCY_FILES.includes(filename))); for (const [relativeFilePath, filename] of dependenciesFiles.entries()) { const fileContent = await retrieveFileContent(relativeFilePath); const contents = { [filename]: fileContent, }; // Check for services (postgres, mongo) if (await hasPostgresDependency(contents, filename)) { genezioConfig = await addServicesToConfig(configPath, { databases: [ { name: await generateDatabaseName("postgres"), type: DatabaseType.neon, }, ], }); frameworksDetected.services = frameworksDetected.services || []; frameworksDetected.services.push({ databases: ["postgres"] }); } if (await hasMongoDependency(contents, filename)) { genezioConfig = await addServicesToConfig(configPath, { databases: [ { name: await generateDatabaseName("mongo"), type: DatabaseType.mongo, }, ], }); frameworksDetected.services = frameworksDetected.services || []; frameworksDetected.services.push({ databases: ["mongo"] }); } } // Extract the environment example files const envExampleFiles = new Map(Array.from(componentFiles).filter(([_, filename]) => ENVIRONMENT_EXAMPLE_FILES.includes(filename))); const resultEnvironmentAnalysis = await analyzeEnvironmentFilesConcurrently(envExampleFiles, genezioConfig); for (const [relativeFilePath, filename] of dependenciesFiles.entries()) { const componentPath = path.dirname(relativeFilePath); const fileContent = await retrieveFileContent(relativeFilePath); const contents = { [filename]: fileContent, }; // Check for frameworks (backend, frontend, ssr, container) if (await isServerlessHttpBackend(contents)) { const entryFile = await findEntryFile(componentPath, contents, SERVERLESS_HTTP_PATTERN, NODE_DEFAULT_ENTRY_FILE); debugLogger.debug("Serverless HTTP entry file found:", entryFile); const isTypescriptFlag = await isTypescript(contents); debugLogger.debug("Is Serverless HTTP typescript:", isTypescriptFlag); let entryFileBuildOut = entryFile; if (isTypescriptFlag && (entryFile.endsWith(".ts") || entryFile.endsWith(".mts") || entryFile.endsWith(".cts"))) { const tsconfigContent = await retrieveFileContent(path.join(componentPath, "tsconfig.json")); // Remove comments before parsing JSON const tsconfigJsonString = tsconfigContent .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1") // Remove comments .replace(/\s*\n\s*/g, "") // Remove newlines and whitespace .trim(); const tsconfigJsonContent = JSON.parse(tsconfigJsonString); entryFileBuildOut = getEntryFileTypescript(entryFile, tsconfigJsonContent); debugLogger.debug("Serverless HTTP entry file build out:", entryFileBuildOut); } const packageManagerType = genezioConfig.backend?.language?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.js, runtime: DEFAULT_NODE_RUNTIME, }, scripts: { deploy: [ `${packageManager.command} install`, isTypescriptFlag ? `${packageManager.command} run build` : "", ].filter(Boolean), local: [ `${packageManager.command} install`, isTypescriptFlag ? `${packageManager.command} run build` : "", ].filter(Boolean), }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), functions: [ { name: "serverless", path: ".", handler: "handler", entry: isTypescriptFlag ? entryFileBuildOut : entryFile, type: FunctionType.aws, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "serverless-http", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isStreamlitComponent(contents)) { const packageManagerType = genezioConfig.streamlit?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER; const entryFile = await findEntryFile(componentPath, contents, STREAMLIT_PATTERN, PYTHON_DEFAULT_ENTRY_FILE); debugLogger.debug("Streamlit entry file found:", entryFile); await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), runtime: DEFAULT_PYTHON_RUNTIME, entryFile: entryFile, }, SSRFrameworkComponentType.streamlit); frameworksDetected.ssr = frameworksDetected.ssr || []; frameworksDetected.ssr.push({ component: "streamlit", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isNestjsComponent(contents)) { const packageManagerType = genezioConfig.nestjs?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), scripts: { deploy: [`${packageManager.command} install`], }, }, SSRFrameworkComponentType.nestjs); frameworksDetected.ssr = frameworksDetected.ssr || []; frameworksDetected.ssr.push({ component: "nestjs", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isExpressBackend(contents)) { const entryFile = await findEntryFile(componentPath, contents, EXPRESS_PATTERN, NODE_DEFAULT_ENTRY_FILE); debugLogger.debug("Express entry file found:", entryFile); const isTypescriptFlag = await isTypescript(contents); debugLogger.debug("Is Express typescript:", isTypescriptFlag); let entryFileBuildOut = entryFile; if (isTypescriptFlag && (entryFile.endsWith(".ts") || entryFile.endsWith(".mts") || entryFile.endsWith(".cts"))) { const tsconfigContent = await retrieveFileContent(path.join(componentPath, "tsconfig.json")); // Remove comments before parsing JSON const tsconfigJsonString = tsconfigContent .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1") // Remove comments .replace(/\s*\n\s*/g, "") // Remove newlines and whitespace .trim(); const tsconfigJsonContent = JSON.parse(tsconfigJsonString); entryFileBuildOut = getEntryFileTypescript(entryFile, tsconfigJsonContent); debugLogger.debug("Express entry file build out:", entryFileBuildOut); } const packageManagerType = genezioConfig.backend?.language?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.js, runtime: DEFAULT_NODE_RUNTIME, }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), scripts: { deploy: [ `${packageManager.command} install`, isTypescriptFlag ? `${packageManager.command} run build` : "", ].filter(Boolean), local: [ `${packageManager.command} install`, isTypescriptFlag ? `${packageManager.command} run build` : "", ].filter(Boolean), }, functions: [ { name: "express", path: ".", entry: isTypescriptFlag ? entryFileBuildOut : entryFile, type: FunctionType.httpServer, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "express", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isFastifyBackend(contents)) { const entryFile = await findEntryFile(componentPath, contents, FASTIFY_PATTERN, NODE_DEFAULT_ENTRY_FILE); debugLogger.debug("Fastify entry file found:", entryFile); const isTypescriptFlag = await isTypescript(contents); debugLogger.debug("Is Fastify typescript:", isTypescriptFlag); let entryFileBuildOut = entryFile; if (isTypescriptFlag && (entryFile.endsWith(".ts") || entryFile.endsWith(".mts") || entryFile.endsWith(".cts"))) { const tsconfigContent = await retrieveFileContent(path.join(componentPath, "tsconfig.json")); // Remove comments before parsing JSON const tsconfigJsonString = tsconfigContent .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1") // Remove comments .replace(/\s*\n\s*/g, "") // Remove newlines and whitespace .trim(); const tsconfigJsonContent = JSON.parse(tsconfigJsonString); entryFileBuildOut = getEntryFileTypescript(entryFile, tsconfigJsonContent); debugLogger.debug("Fastify entry file build out:", entryFileBuildOut); } const packageManagerType = genezioConfig.backend?.language?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.js, runtime: DEFAULT_NODE_RUNTIME, }, scripts: { deploy: [ `${packageManager.command} install`, isTypescriptFlag ? `${packageManager.command} run build` : "", ].filter(Boolean), local: [ `${packageManager.command} install`, isTypescriptFlag ? `${packageManager.command} run build` : "", ].filter(Boolean), }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), functions: [ { name: "fastify", path: ".", entry: isTypescriptFlag ? entryFileBuildOut : entryFile, type: FunctionType.httpServer, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "fastify", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isFlaskComponent(contents)) { const entryFile = await findEntryFile(componentPath, contents, FLASK_PATTERN, PYTHON_DEFAULT_ENTRY_FILE); const fullpath = path.join(componentPath, entryFile); const entryFileContent = await retrieveFileContent(fullpath); const pythonHandler = getPythonHandler(entryFileContent); const packageManagerType = genezioConfig.backend?.language?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.python, packageManager: packageManagerType, runtime: DEFAULT_PYTHON_RUNTIME, }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), functions: [ { name: "flask", path: ".", handler: pythonHandler, entry: entryFile, type: FunctionType.httpServer, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "flask", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isDjangoComponent(contents)) { const entryFile = await findEntryFile(componentPath, contents, DJANGO_PATTERN, "wsgi.py"); const packageManagerType = genezioConfig.backend?.language?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.python, packageManager: packageManagerType, runtime: DEFAULT_PYTHON_RUNTIME, }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), functions: [ { name: "django", path: ".", handler: "application", entry: entryFile, type: FunctionType.httpServer, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "django", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isFastAPIComponent(contents)) { const entryFile = await findEntryFile(componentPath, contents, FASTAPI_PATTERN, PYTHON_DEFAULT_ENTRY_FILE); const fullpath = path.join(componentPath, entryFile); const entryFileContent = await retrieveFileContent(fullpath); const pythonHandler = getPythonHandler(entryFileContent); const packageManagerType = genezioConfig.backend?.language?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.python, packageManager: packageManagerType, runtime: DEFAULT_PYTHON_RUNTIME, }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), functions: [ { name: "fastapi", path: ".", handler: pythonHandler, entry: entryFile, type: FunctionType.httpServer, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "fastapi", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isPythonLambdaFunction(contents)) { const entryFile = await findEntryFile(componentPath, contents, PYTHON_LAMBDA_PATTERN, PYTHON_DEFAULT_ENTRY_FILE); const packageManagerType = genezioConfig.backend?.language?.packageManager || PYTHON_DEFAULT_PACKAGE_MANAGER; await addBackendComponentToConfig(configPath, { path: componentPath, language: { name: Language.python, packageManager: packageManagerType, runtime: DEFAULT_PYTHON_RUNTIME, }, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), functions: [ { name: "serverless", path: ".", handler: "handler", entry: entryFile, type: FunctionType.aws, }, ], }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "python-lambda", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isNitroComponent(contents)) { const packageManagerType = genezioConfig.nitro?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), scripts: { deploy: [`${packageManager.command} install`], }, }, SSRFrameworkComponentType.nitro); frameworksDetected.ssr = frameworksDetected.ssr || []; frameworksDetected.ssr.push({ component: "nitro", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isNextjsComponent(contents)) { const packageManagerType = genezioConfig.nestjs?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), scripts: { deploy: [`${packageManager.command} install`], }, }, SSRFrameworkComponentType.next); frameworksDetected.ssr = frameworksDetected.ssr || []; frameworksDetected.ssr.push({ component: "next", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isNuxtComponent(contents)) { const packageManagerType = genezioConfig.nuxt?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), scripts: { deploy: [`${packageManager.command} install`], }, }, SSRFrameworkComponentType.nuxt); frameworksDetected.ssr = frameworksDetected.ssr || []; frameworksDetected.ssr.push({ component: "nuxt", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isRemixComponent(contents)) { const packageManagerType = genezioConfig.remix?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, environment: mapEnvironmentVariableToConfig(resultEnvironmentAnalysis.get(componentPath)?.environmentVariables), scripts: { build: [`${packageManager.command} run build`], deploy: [ `${packageManager.command} install`, `${packageManager.command} run build`, ], }, }, SSRFrameworkComponentType.remix); frameworksDetected.ssr = frameworksDetected.ssr || []; frameworksDetected.ssr.push({ component: "remix", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isVueComponent(contents)) { const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addFrontendComponentToConfig(configPath, { path: componentPath, publish: path.join(componentPath, "dist"), scripts: { deploy: [`${packageManager.command} install`], build: [`${packageManager.command} run build`], start: [ `${packageManager.command} install`, `${packageManager.command} run dev`, ], }, }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "vue", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isAngularComponent(contents)) { const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addFrontendComponentToConfig(configPath, { path: componentPath, publish: path.join("dist", "browser"), scripts: { deploy: [`${packageManager.command} install`], build: [`${packageManager.command} run build`], start: [`${packageManager.command} install`, `${packageManager.command} start`], }, }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "angular", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isSvelteComponent(contents)) { const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addFrontendComponentToConfig(configPath, { path: componentPath, publish: "dist", scripts: { deploy: [`${packageManager.command} install`], build: [`${packageManager.command} run build`], start: [ `${packageManager.command} install`, `${packageManager.command} run dev`, ], }, }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "svelte", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isEmberComponent(contents)) { debugLogger.info("Ember component detected"); const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addFrontendComponentToConfig(configPath, { path: componentPath, publish: "dist", scripts: { deploy: [`${packageManager.command} install`], build: [`${packageManager.command} run build`], start: [ `${packageManager.command} install`, `${packageManager.command} run start`, ], }, }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "ember", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isViteComponent(contents)) { const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addFrontendComponentToConfig(configPath, { path: componentPath, publish: "dist", scripts: { deploy: [`${packageManager.command} install`], build: [`${packageManager.command} run build`], start: [ `${packageManager.command} install`, `${packageManager.command} run dev`, ], }, }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "vite", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isReactComponent(contents)) { const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addFrontendComponentToConfig(configPath, { path: componentPath, publish: "build", scripts: { deploy: [`${packageManager.command} install`], build: [`${packageManager.command} run build`], start: [`${packageManager.command} install`, `${packageManager.command} start`], }, }); frameworksDetected.frontend = frameworksDetected.frontend || []; frameworksDetected.frontend.push({ component: "react", environment: resultEnvironmentAnalysis.get(componentPath)?.environmentVariables, }); continue; } if (await isGenezioTypesafe(contents)) { const packageManagerType = NODE_DEFAULT_PACKAGE_MANAGER; const packageManager = packageManagers[packageManagerType]; await addBackendComponentToConfig(configPath, { path: componentPath, // TODO: Add support for detecting the language of the backend language: { name: Language.ts, runtime: DEFAULT_NODE_RUNTIME, }, scripts: { deploy: [`${packageManager.command} install`], local: [`${packageManager.command} install`], }, }); frameworksDetected.backend = frameworksDetected.backend || []; frameworksDetected.backend.push({ component: "genezio-typesafe" }); continue; } } // TODO - Delete this when support for functions (express, flask etc) with nextjs, nuxt, remix is added // If backend and ssr was detected, remove ssr to make sure this is still deployable if (frameworksDetected.backend && frameworksDetected.ssr) { frameworksDetected.ssr = undefined; await handleBackendAndSSRConfig(configPath); } // Inject backend URLs into the frontend components like frontend, nextjs, nuxts // This is done after all the components have been detected // TODO Support multiple frontend frameworks in the same project await injectBackendUrlsInConfig(configPath); if (frameworksDetected.backend?.some((entry) => entry.component.includes("genezio-typesafe")) && frameworksDetected.frontend && frameworksDetected.frontend.length > 0) { // TODO Support multiple frontend frameworks in the same project await injectSDKInConfig(configPath); } // Report the detected frameworks at stdout const result = report(format, frameworksDetected, genezioConfig); log.info(result); } async function analyzeEnvironmentFilesConcurrently(envExampleFiles, genezioConfig) { if (envExampleFiles.size === 0) { return new Map(); } const envExampleContents = new Map(); const resultEnvironmentAnalysis = new Map(); await Promise.all(Array.from(envExampleFiles.entries()).map(async ([relativeFilePath, filename]) => { const componentPath = path.dirname(relativeFilePath); const fileContent = await retrieveFileContent(relativeFilePath); envExampleContents.set(filename, fileContent); // Analyze the environment example file const environmentVariablesAnalysis = await analyzeEnvironmentVariableExampleFile(fileContent, genezioConfig.services); if (environmentVariablesAnalysis.length === 0) { return; } resultEnvironmentAnalysis.set(componentPath, { environmentVariables: environmentVariablesAnalysis, }); })); return resultEnvironmentAnalysis; } /** * Filters and transforms environment variables from a list of ProjectEnvironment. * @param envAnalysis - Array of ProjectEnvironment objects. * @returns A record mapping environment keys to default values. */ function mapEnvironmentVariableToConfig(environmentVariables) { if (!environmentVariables || environmentVariables.length === 0) { return {}; } return environmentVariables .filter((env) => /\$\{\{.*\}\}/.test(env.defaultValue)) .reduce((acc, env) => { acc[env.key] = env.defaultValue; return acc; }, {}); } /** * This is costly so we should try to call it as few times as possible */ export const findKeyFiles = async (dir, keyFiles = KEY_FILES) => { const result = new Map(); const searchDir = async (currentDir) => { const entries = await fs.readdir(currentDir, { withFileTypes: true }); await Promise.all(entries.map(async (entry) => { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { // Skip excluded directories if (EXCLUDED_DIRECTORIES.includes(entry.name)) return; // Recursively search subdirectory await searchDir(fullPath); } else if (keyFiles.includes(entry.name)) { // If the file is one of the key files, add it to the map const relativePath = path.relative(dir, fullPath); result.set(relativePath, entry.name); } })); }; await searchDir(dir); return result; }; // Method to read the contents of a file async function retrieveFileContent(filePath) { try { await fs.access(filePath); } catch (error) { return ""; } try { // Read and return the file content const fileContent = await fs.readFile(filePath, "utf-8"); return fileContent; } catch (error) { log.error(`Error reading file at ${filePath}: ${error}`); return ""; } }