UNPKG

genezio

Version:

Command line utility to interact with Genezio infrastructure.

667 lines (666 loc) 35.5 kB
import git from "isomorphic-git"; import { log } from "../../utils/logging.js"; import path from "path"; import { exit } from "process"; import { DASHBOARD_URL, RECOMMENTDED_GENEZIO_TYPES_VERSION_RANGE, REQUIRED_GENEZIO_TYPES_VERSION_RANGE, } from "../../constants.js"; import { GENEZIO_NO_CLASSES_FOUND, GENEZIO_NOTHING_TO_DEPLOY, UserError } from "../../errors.js"; import { mapYamlClassToSdkClassConfiguration, sdkGeneratorApiHandler, } from "../../generateSdk/generateSdkApi.js"; import { ProjectConfiguration } from "../../models/projectConfiguration.js"; import { getNoMethodClasses } from "../../utils/getNoMethodClasses.js"; import { fileExists, createTemporaryFolder, zipDirectory, isDirectoryEmpty, directoryContainsIndexHtmlFiles, directoryContainsHtmlFiles, deleteFolder, getBundleFolderSizeLimit, zipFile, } from "../../utils/file.js"; import { printAdaptiveLog, debugLogger, doAdaptiveLogAction } from "../../utils/logging.js"; import { GenezioCommand, reportSuccess, reportSuccessFunctions } from "../../utils/reporter.js"; import { generateRandomSubdomain } from "../../utils/yaml.js"; import cliProgress from "cli-progress"; import { GenezioCloudAdapter } from "../../cloudAdapter/genezio/genezioAdapter.js"; import { GenezioCloudInputType, GenezioFunctionMetadataType, } from "../../cloudAdapter/cloudAdapter.js"; import { CloudProviderIdentifier } from "../../models/cloudProviderIdentifier.js"; import { GenezioTelemetry, TelemetryEventTypes } from "../../telemetry/telemetry.js"; import colors from "colors"; import { bundle } from "../../bundlers/utils.js"; import { isDependencyVersionCompatible } from "../../utils/jsProjectChecker.js"; import { YamlConfigurationIOController } from "../../projectConfiguration/yaml/v2.js"; import { entryFileFunctionMap, FunctionType, Language, } from "../../projectConfiguration/yaml/models.js"; import { runScript } from "../../utils/scripts.js"; import { scanClassesForDecorators } from "../../utils/configuration.js"; import configIOController from "../../projectConfiguration/yaml/v2.js"; import { ClusterCloudAdapter } from "../../cloudAdapter/cluster/clusterAdapter.js"; import { writeSdk } from "../../generateSdk/sdkWriter/sdkWriter.js"; import { reportSuccessForSdk } from "../../generateSdk/sdkSuccessReport.js"; import { isLoggedIn } from "../../utils/accounts.js"; import { loginCommand } from "../login.js"; import fsExtra from "fs-extra/esm"; import { getLinkedFrontendsForProject } from "../../utils/linkDatabase.js"; import { getCloudProvider } from "../../requests/getCloudProvider.js"; import fs, { mkdirSync } from "fs"; import { createBackendEnvVarList, uploadUserCode, evaluateResource, prepareServicesPreBackendDeployment, prepareServicesPostBackendDeployment, excludedFiles, actionDetectedEnvFile, createBackendEnvVarListFromRemote, EnvironmentResourceType, } from "./utils.js"; import { expandEnvironmentVariables, warningMissingEnvironmentVariables, } from "../../utils/environmentVariables.js"; import { getFunctionHandlerProvider } from "../../utils/getFunctionHandlerProvider.js"; import { getFunctionEntryFilename } from "../../utils/getFunctionEntryFilename.js"; import { syncCrons } from "../../requests/crons.js"; import { packageManagers, PYTHON_DEFAULT_PACKAGE_MANAGER, } from "../../packageManagers/packageManager.js"; import { DEFAULT_PYTHON_RUNTIME, DEFAULT_PYTHON_VERSION_INSTALL, } from "../../models/projectOptions.js"; import { detectPythonVersion } from "../../utils/detectPythonCommand.js"; import { isCI } from "../../utils/process.js"; import { deployRequest } from "../../requests/deployCode.js"; export async function genezioDeploy(options) { const configIOController = new YamlConfigurationIOController(options.config, { stage: options.stage, }); const configuration = await configIOController.read(); if (!configuration.backend && !configuration.frontend) { log.warn(GENEZIO_NOTHING_TO_DEPLOY); return; } const backendCwd = configuration.backend?.path || process.cwd(); // Give the user another chance if he forgot to add `--env` flag if (!isCI() && !options.env) { options.env = await actionDetectedEnvFile(backendCwd, configuration.name, options.stage); } // We need to check if the user is using an older version of @genezio/types // because we migrated the decorators implemented in the @genezio/types package to the stage 3 implementation. // Otherwise, the user will get an error at runtime. This check can be removed in the future once no one is using version // 0.1.* of @genezio/types. if (configuration.backend?.language.name === Language.ts || configuration.backend?.language.name === Language.js) { const packageJsonPath = path.join(backendCwd, "package.json"); if (isDependencyVersionCompatible(packageJsonPath, "@genezio/types", REQUIRED_GENEZIO_TYPES_VERSION_RANGE) === false) { throw new UserError(`You are currently using an older version of @genezio/types, which is not compatible with this version of the genezio CLI. To solve this, please update the @genezio/types package on your backend component using the following command: npm install @genezio/types@${RECOMMENTDED_GENEZIO_TYPES_VERSION_RANGE}`); } } // check if user is logged in if (!(await isLoggedIn())) { debugLogger.debug("No auth token found. Starting automatic authentication..."); await loginCommand("", false); } const projectName = configuration.name; await prepareServicesPreBackendDeployment(configuration, projectName, options.stage, options.env); let deployClassesResult; backend: if (configuration.backend && !options.frontend) { if (configuration.backend.classes?.length === 0) { log.error("No classes were found in your genezio.yaml. Add some to be able to deploy your backend."); break backend; } await doAdaptiveLogAction("Running backend deploy scripts", async () => { await runScript(configuration.backend?.scripts?.deploy, backendCwd); }).catch(async (error) => { await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_PRE_BACKEND_DEPLOY_SCRIPT_ERROR, commandOptions: JSON.stringify(options), }); throw error; }); await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_BACKEND_DEPLOY_START, commandOptions: JSON.stringify(options), }); deployClassesResult = await deployClasses(configuration, options).catch(async (error) => { await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_BACKEND_DEPLOY_ERROR, errorTrace: error.toString(), commandOptions: JSON.stringify(options), }); throw error; }); await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_BACKEND_DEPLOY_END, commandOptions: JSON.stringify(options), }); if (deployClassesResult) { await warningMissingEnvironmentVariables(backendCwd, deployClassesResult?.projectId, deployClassesResult?.projectEnvId); } } const frontendUrls = []; if (configuration.frontend && !options.backend) { const frontends = configuration.frontend; // If name was provided, validate it exists before attempting deployments if (options.name) { const matchingFrontend = frontends.find((f) => f.name === options.name); if (!matchingFrontend) { const availableFrontends = frontends .filter((f) => f.name) .map((f) => f.name) .join(", "); throw new UserError(`No frontend found with name: ${options.name}. Available frontends: ${availableFrontends || "none"}`); } } for (const [index, frontend] of frontends.entries()) { // Skip if name option is provided and doesn't match the current frontend if (options.name) { if (!frontend.name) { log.warn(`Frontend #${index + 1} has no name specified in genezio.yaml. Consider adding a name to improve deployment filtering.`); continue; } if (frontend.name !== options.name) { continue; } } try { await doAdaptiveLogAction(`Running frontend ${index + 1} deploy script`, async () => { const newEnvObject = await expandEnvironmentVariables(frontend.environment, configuration, options.stage, /* envFile */ undefined, { isFrontend: true, }); debugLogger.debug(`Environment variables injected for frontend.scripts.deploy:`, JSON.stringify(newEnvObject)); return await runScript(frontend.scripts?.deploy, frontend.path || process.cwd(), newEnvObject); }); } catch (error) { await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_PRE_FRONTEND_DEPLOY_SCRIPT_ERROR, commandOptions: JSON.stringify(options), }); throw error; } await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_FRONTEND_DEPLOY_START, commandOptions: JSON.stringify(options), }); log.info("Deploying your frontend to the genezio infrastructure..."); const frontendUrl = await deployFrontend(configuration.name, configuration.region, frontend, index, options, configuration).catch(async (error) => { if (error instanceof Error) { if (error.message == "No frontend entry in genezio configuration file.") { log.error(error.message); exit(0); } await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_FRONTEND_DEPLOY_ERROR, errorTrace: error.toString(), commandOptions: JSON.stringify(options), }); throw error; } }); if (frontendUrl) frontendUrls.push(frontendUrl); await GenezioTelemetry.sendEvent({ eventType: TelemetryEventTypes.GENEZIO_FRONTEND_DEPLOY_END, commandOptions: JSON.stringify(options), }); } } // Sync cron jobs after the project is created if (configuration.services?.crons) { printAdaptiveLog("Deploying cron jobs\n", "start"); const crons = []; for (const cron of configuration.services.crons) { const yamlFunctionName = await evaluateResource(configuration, [ EnvironmentResourceType.RemoteResourceReference, EnvironmentResourceType.LiteralValue, ], cron.function, options.stage || "prod", /* envFile */ undefined, /* options */ undefined); if (!deployClassesResult) { throw new UserError("Could not deploy cron jobs. Please make sure your backend is deployed before adding cron jobs."); } const functions = deployClassesResult.functions ?? []; const cronFunction = functions.find((f) => f.name === `function-${yamlFunctionName}`); if (!cronFunction) { throw new UserError(`Function ${yamlFunctionName} not found. Please make sure the function is deployed before adding cron jobs.`); } crons.push({ name: cron.name, url: cronFunction.cloudUrl, endpoint: cron.endpoint || "", cronString: cron.schedule, }); } await syncCrons({ projectName: projectName, stageName: options.stage || "prod", crons: crons, }).catch((error) => { printAdaptiveLog("Deploying cron jobs\n", "error"); throw new UserError(`Something went wrong while syncing the cron jobs.\n${error}\nPlease try to redeploy your project. If the problem persists, please contact support at contact@genez.io.`); }); printAdaptiveLog("Cron Jobs deployed successfully\n", "end"); } else { await syncCrons({ projectName: projectName, stageName: options.stage || "prod", crons: [], }).catch(() => { throw new UserError("Something went wrong while syncing the cron jobs. Please try to redeploy your project. If the problem persists, please contact support at contact@genez.io."); }); } await uploadUserCode(configuration.name, configuration.region, options.stage, process.cwd()); await prepareServicesPostBackendDeployment(configuration, projectName, options.stage); if (deployClassesResult) { log.info(colors.cyan(`App Dashboard URL: ${DASHBOARD_URL}/project/${deployClassesResult.projectId}/${deployClassesResult.projectEnvId}`)); } for (const frontendUrl of frontendUrls) { log.info(colors.cyan(`Frontend URL: ${frontendUrl}`)); } } export async function deployClasses(configuration, options) { const backend = configuration.backend; backend.classes = await scanClassesForDecorators(backend); backend.functions = backend.functions ?? []; if (backend.classes.length === 0 && backend.functions?.length === 0) { throw new UserError(GENEZIO_NO_CLASSES_FOUND(backend.language.name)); } const stack = []; if (backend.classes.length > 0) { // append typesafe to the stack if there are classes stack.push("typesafe"); } const sdkLanguages = []; // Add configuration frontends that contain the SDK field sdkLanguages.push(...(configuration.frontend || []) .map((f) => f.sdk?.language) .filter((f) => f !== undefined)); // Add linked frontends sdkLanguages.push(...(await getLinkedFrontendsForProject(configuration.name)).map((f) => f.language)); const sdkResponse = await sdkGeneratorApiHandler(sdkLanguages, mapYamlClassToSdkClassConfiguration(backend.classes, backend.language.name, backend.path), backend.path, /* packageName= */ `@genezio-sdk/${configuration.name}`).catch((error) => { // TODO: this is not very generic error handling. The SDK should throw Genezio errors, not babel. if (error.code === "BABEL_PARSER_SYNTAX_ERROR") { log.error("Syntax error:"); log.error(`Reason Code: ${error.reasonCode}`); log.error(`File: ${error.path}:${error.loc.line}:${error.loc.column}`); } throw error; }); const cloudProvider = // TODO: Remove this as soon as Genezio Cloud supports Go backend.language.name === Language.go ? CloudProviderIdentifier.GENEZIO_AWS : await getCloudProvider(configuration.name); const projectConfiguration = new ProjectConfiguration(configuration, cloudProvider, sdkResponse); const classesWithNoMethods = getNoMethodClasses(projectConfiguration.classes); if (classesWithNoMethods.length) { const errorClasses = classesWithNoMethods.join(", "); throw new UserError(`Unable to deploy classes [${errorClasses}] as they do not have any methods.`); } new cliProgress.MultiBar({ clearOnComplete: false, hideCursor: true, format: "Uploading {filename}: {bar} | {value}% | {eta_formatted}", }, cliProgress.Presets.shades_grey); printAdaptiveLog("Bundling your code\n", "start"); const bundlerResult = projectConfiguration.classes.map(async (element) => { const ast = sdkResponse.classesInfo.find((classInfo) => classInfo.classConfiguration.path === element.path).program; const output = await bundle(projectConfiguration, ast, element, options.installDeps, options.disableOptimization); const unzippedBundleSize = await getBundleFolderSizeLimit(output.path); debugLogger.debug(`The unzippedBundleSize for class ${element.path} is ${unzippedBundleSize}.`); // .jar files cannot be parsed by AWS Lambda, skip this step for AWS Lambda if (element.language === "kt") { debugLogger.debug("Skipping ZIP due to .jar file"); debugLogger.debug(path.join(output.path, "app-standalone.jar")); return { type: GenezioCloudInputType.CLASS, name: element.name, archivePath: path.join(output.path, "app-standalone.jar"), filePath: element.path, methods: element.methods, unzippedBundleSize, entryFile: output.extra.entryFile ?? "app-standalone.jar", }; } const archivePathTempFolder = await createTemporaryFolder(); const archivePath = path.join(archivePathTempFolder, `genezioDeploy.zip`); debugLogger.debug(`Zip the directory ${output.path}.`); if (element.language === "go") { await zipFile(path.join(output.path, "bootstrap"), archivePath); } else { await zipDirectory(output.path, archivePath, true, [".git", ".github"]); } await deleteFolder(output.path); return { type: GenezioCloudInputType.CLASS, name: element.name, archivePath: archivePath, filePath: element.path, methods: element.methods, dependenciesInfo: output.extra.dependenciesInfo, allNonJsFilesPaths: output.extra.allNonJsFilesPaths, unzippedBundleSize: unzippedBundleSize, entryFile: output.extra.entryFile ?? "", timeout: element.timeout, storageSize: element.storageSize, instanceSize: element.instanceSize, vcpuCount: element.vcpuCount, memoryMb: element.memoryMb, maxConcurrentRequestsPerInstance: element.maxConcurrentRequestsPerInstance, maxConcurrentInstances: element.maxConcurrentInstances, cooldownTime: element.cooldownTime, persistent: element.persistent, }; }); const functionsResultArray = projectConfiguration.functions.map((f) => functionToCloudInput(f, backend.path, /* outputDir */ undefined, backend.language.runtime)); const cloudAdapterDeployInput = await Promise.all([ ...bundlerResult, ...functionsResultArray, ]).catch((error) => { printAdaptiveLog("Bundling your code\n", "error"); throw error; }); printAdaptiveLog("Bundling your code\n", "end"); projectConfiguration.astSummary.classes = projectConfiguration.astSummary.classes.map((c) => { // remove cwd from path and the extension return { ...c, path: path.relative(process.cwd(), c.path).replace(/\.[^/.]+$/, ""), }; }); projectConfiguration.classes = projectConfiguration.classes.map((c) => { // remove cwd from path and the extension return { ...c, path: path.relative(process.cwd(), c.path).replace(/\.[^/.]+$/, ""), }; }); const projectGitRepositoryUrl = (await git.listRemotes({ fs, dir: process.cwd() })).find((r) => r.remote === "origin")?.url; debugLogger.debug(`Sending prepare request`); const prepareResult = await deployRequest(projectConfiguration, cloudAdapterDeployInput, options.stage, stack, projectGitRepositoryUrl, undefined, true); const environmentVariables = await createBackendEnvVarList(options.env, options.stage, configuration); if (prepareResult.projectId) { if (configuration.backend?.environment) { const environmentVariablesFromRemote = await createBackendEnvVarListFromRemote(configuration.backend?.environment, configuration, options.stage || "prod"); environmentVariables.push(...environmentVariablesFromRemote); } } // TODO: Enable cloud adapter setting for every class const cloudAdapter = getCloudAdapter(cloudProvider); const result = await cloudAdapter.deploy(cloudAdapterDeployInput, projectConfiguration, { stage: options.stage, }, stack, /* sourceRepository= */ projectGitRepositoryUrl, /* environmentVariables= */ environmentVariables); if (sdkResponse.generatorResponses.length > 0 && sdkResponse.generatorResponses[0].files.length <= 0) { log.info(colors.cyan("Your backend code was successfully deployed!")); return; } else { log.info(colors.cyan(`Your backend code was ${result.classes.length > 0 ? "deployed and the SDK was successfully generated" : "successfully deployed"}`)); } if (result.classes.length > 0) { await handleSdk(configuration, result, sdkResponse, options); reportSuccess(result.classes); } if (result.functions.length > 0) { reportSuccessFunctions(result.functions); } const projectId = result.projectId; const projectEnvId = result.projectEnvId; if (projectId) { return { projectId: projectId, projectEnvId: projectEnvId, functions: result.functions, classes: result.classes, }; } } export async function functionToCloudInput(functionElement, backendPath, outputDir, runtime) { const supportedFunctionLanguages = [ Language.js, Language.ts, Language.python, Language.pythonAsgi, ]; if (!supportedFunctionLanguages.includes(functionElement.language)) { throw new UserError(`The language ${functionElement.language} is not supported for functions. Supported languages are: ${supportedFunctionLanguages.join(", ")}`); } const handlerProvider = getFunctionHandlerProvider(functionElement.type, functionElement.language); if (outputDir && !fs.existsSync(outputDir)) { debugLogger.debug(`Creating output directory ${outputDir}`); mkdirSync(outputDir, { recursive: true }); } const tmpFolderPath = await createTemporaryFolder(); const archivePath = path.join(outputDir || (await createTemporaryFolder()), `genezioDeploy.zip`); if (functionElement.language === Language.python || functionElement.language === Language.pythonAsgi) { await fsExtra.copy(path.join(backendPath, functionElement.path), tmpFolderPath, { filter: (src) => { const relativePath = path.relative(path.join(backendPath, functionElement.path), src); return !excludedFiles.some((pattern) => relativePath.includes(pattern)); }, }); } else { // copy everything to the temporary folder await fsExtra.copy(path.join(backendPath, functionElement.path), tmpFolderPath); } // Handle JS/TS functions with pnpm if (functionElement.language === "js" || functionElement.language === "ts") { if (fsExtra.pathExistsSync(path.join(tmpFolderPath, "node_modules", ".pnpm"))) { await fsExtra.remove(path.join(tmpFolderPath, "node_modules", ".pnpm")); await fsExtra.copy(path.join(backendPath, functionElement.path, "node_modules", ".pnpm"), path.join(tmpFolderPath, "node_modules", ".pnpm"), { dereference: true, }); } } let metadata = { healthcheck_path: functionElement.healthcheckPath, }; // Handle Python projects dependencies // asgilinux-3.13 // pythonlinux-3.13 if (functionElement.language === Language.python || functionElement.language === Language.pythonAsgi) { metadata = { type: GenezioFunctionMetadataType.Python, app_name: functionElement.handler, healthcheck_path: functionElement.healthcheckPath, }; const requirementsPath = path.join(backendPath, functionElement.path, "requirements.txt"); const pyProjectTomlPath = path.join(backendPath, functionElement.path, "pyproject.toml"); if (fs.existsSync(requirementsPath) || fs.existsSync(pyProjectTomlPath)) { const pathForDependencies = path.join(tmpFolderPath, "packages"); const packageManager = packageManagers[PYTHON_DEFAULT_PACKAGE_MANAGER]; let installCommand; const versionMatch = (runtime || DEFAULT_PYTHON_RUNTIME).match(/\d+\.\d+/); const pythonVersion = versionMatch ? versionMatch[0] : DEFAULT_PYTHON_VERSION_INSTALL; // check pythonVersion matches with local python version const localPythonVersion = (await detectPythonVersion())?.match(/\d+\.\d+/)?.[0] || DEFAULT_PYTHON_VERSION_INSTALL; if (pythonVersion !== localPythonVersion) { const warningMessage = [ `Python Version Mismatch`, ``, `We noticed you're using different Python versions locally and in production:`, `Local Environment: Python ${localPythonVersion}`, `Production Runtime: Python ${pythonVersion}`, ``, `This might lead to unexpected behavior. To ensure consistency, update your genezio.yaml configuration to match your local version:`, ``, `runtime: python${localPythonVersion}.x`, ``, `This will help prevent potential compatibility issues!`, `For complete yaml configuration, visit: https://genezio.com/docs/project-structure/genezio-configuration-file/`, ].join("\n"); log.warn(colors.yellow(warningMessage)); } if (packageManager.command === "pip" || packageManager.command === "pip3") { if (fs.existsSync(requirementsPath)) { const requirementsOutputPath = path.join(tmpFolderPath, "requirements.txt"); const requirementsContent = fs .readFileSync(requirementsOutputPath, "utf8") .trim(); if (requirementsContent) { installCommand = `${packageManager.command} install -r ${requirementsOutputPath} --platform manylinux2014_x86_64 --only-binary=:all: --python-version ${pythonVersion} -t ${pathForDependencies} --no-user`; } } else if (fs.existsSync(pyProjectTomlPath)) { installCommand = `${packageManager.command} install ${path.join(tmpFolderPath)} --platform manylinux2014_x86_64 --only-binary=:all: --python-version ${pythonVersion} -t ${pathForDependencies}`; } } else if (packageManager.command === "poetry") { installCommand = `${packageManager.command} install --no-root --directory ${pathForDependencies}`; } else { throw new UserError(`Unsupported package manager: ${packageManager.command}`); } if (installCommand) { debugLogger.debug(`Installing dependencies using command: ${installCommand}`); await runScript(installCommand, tmpFolderPath); } else { debugLogger.debug("No valid requirements.txt or pyproject.toml found."); } } } const unzippedBundleSize = await getBundleFolderSizeLimit(tmpFolderPath); // Determine entry file name let entryFileName; if (functionElement.type === FunctionType.httpServer || functionElement.type === FunctionType.persistent) { entryFileName = path.join(functionElement.entry).replace(/\\/g, "/"); } else { entryFileName = entryFileFunctionMap[functionElement.language]; while (fs.existsSync(path.join(tmpFolderPath, entryFileName))) { debugLogger.debug(`[FUNCTION ${functionElement.name}] File ${entryFileName} already exists in the temporary folder.`); entryFileName = getFunctionEntryFilename(functionElement.language, `index-${Math.random().toString(36).substring(7)}`); } if (handlerProvider) { await handlerProvider.write(tmpFolderPath, entryFileName, functionElement); } } debugLogger.debug(`Zip the directory ${tmpFolderPath}.`); // zip the temporary folder await zipDirectory(tmpFolderPath, archivePath, true, [".git", ".github"]); debugLogger.debug(`Zip created at path: ${archivePath}.`); await deleteFolder(tmpFolderPath); return { type: GenezioCloudInputType.FUNCTION, name: functionElement.name, archivePath: archivePath, unzippedBundleSize: unzippedBundleSize, entryFile: entryFileName, timeout: functionElement.timeout, instanceSize: functionElement.instanceSize, vcpuCount: functionElement.vcpuCount, memoryMb: functionElement.memoryMb, storageSize: functionElement.storageSize, maxConcurrentRequestsPerInstance: functionElement.maxConcurrentRequestsPerInstance, maxConcurrentInstances: functionElement.maxConcurrentInstances, cooldownTime: functionElement.cooldownTime, persistent: functionElement.type === FunctionType.persistent, metadata: metadata, }; } export async function deployFrontend(name, region, frontend, index, options, configuration) { const stage = options.stage || ""; if (frontend.publish === null || frontend.publish === undefined) { log.info(`Skipping frontend deployment for \`${frontend.path}\` because it has no publish folder in the YAML configuration. Check https://genezio.com/docs/project-structure/genezio-configuration-file for more details.`); return; } await doAdaptiveLogAction(`Building frontend ${index + 1}`, async () => { const newEnvObject = await expandEnvironmentVariables(frontend.environment, configuration, options.stage, /* envFile */ undefined, { isFrontend: true, }); debugLogger.debug(`Environment variables injected for frontend.scripts.build:`, JSON.stringify(newEnvObject)); await runScript(frontend.scripts?.build, frontend.path, newEnvObject); }); // check if the frontend publish path exists if (!(await fileExists(path.join(frontend.path, frontend.publish)))) { throw new UserError(`The frontend path ${colors.cyan(`${frontend.publish}`)} does not exist. Please make sure the path is correct.`); } // check if subdomain contains only numbers, letters and hyphens if (frontend.subdomain && !frontend.subdomain.match(/^[a-zA-z0-9][a-zA-Z0-9-]{0,62}$/)) { throw new UserError(`The subdomain can only contain letters, numbers and hyphens.`); } // check if the publish folder exists const frontendPath = path.join(frontend.path, frontend.publish); if (!(await fileExists(frontendPath))) { throw new UserError(`The publish folder ${colors.cyan(`${frontendPath}`)} does not exist. Please run the build command first or add a \`deploy\` script in the genezio.yaml file.`); } // check if the publish folder is empty if (await isDirectoryEmpty(frontendPath)) { throw new UserError(`The publish folder ${colors.cyan(`${frontendPath}`)} is empty. Please run the build command first or add a \`deploy\` script in the genezio.yaml file.`); } // check if there are any .html files in the publish folder if (!(await directoryContainsHtmlFiles(frontendPath))) { log.info("WARNING: No .html files found in the publish folder"); } else if (!(await directoryContainsIndexHtmlFiles(frontendPath))) { // check if there is no index.html file in the publish folder log.info("WARNING: No index.html file found in the publish folder"); } if (!options.subdomain && !frontend.subdomain) { log.info("No subdomain specified in the genezio.yaml configuration file or as an option flag. We will provide a random one for you."); // write the configuration in yaml file const yamlConfigIOController = new YamlConfigurationIOController(options.config); const yamlConfig = await yamlConfigIOController.read(/* fillDefaults= */ false); if (yamlConfig.frontend) { const subdomain = generateRandomSubdomain(); if (Array.isArray(yamlConfig.frontend)) { yamlConfig.frontend[index].subdomain = subdomain; } else { yamlConfig.frontend.subdomain = subdomain; } frontend.subdomain = subdomain; } else { throw new UserError("No frontend entry in genezio configuration file."); } await configIOController.write(yamlConfig); } frontend.subdomain = options.subdomain || frontend.subdomain; const cloudAdapter = getCloudAdapter(CloudProviderIdentifier.GENEZIO_CLOUD); const url = await cloudAdapter.deployFrontend(name, region, frontend, stage); return url; } async function handleSdk(configuration, result, sdk, options) { const frontends = configuration.frontend; const sdkLocations = []; for (const frontend of frontends || []) { if (frontend.sdk) { sdkLocations.push({ path: path.join(frontend.path, frontend.sdk.path || "sdk"), language: frontend.sdk.language, }); } } const linkedFrontends = await getLinkedFrontendsForProject(configuration.name); linkedFrontends.forEach((f) => sdkLocations.push({ path: f.path, language: f.language, })); for (const sdkLocation of sdkLocations) { const sdkResponse = sdk.generatorResponses.find((response) => response.sdkGeneratorInput.language === sdkLocation.language); if (!sdkResponse) { throw new UserError("Could not find the SDK for the frontend."); } const classUrls = result.classes.map((c) => ({ name: c.className, cloudUrl: c.functionUrl, })); await writeSdk({ language: sdkLocation.language, packageName: `@genezio-sdk/${configuration.name}`, packageVersion: `1.0.0-${options.stage}`, sdkResponse, classUrls, publish: true, installPackage: true, outputPath: sdkLocation.path, }); reportSuccessForSdk(sdkLocation.language, sdkResponse, GenezioCommand.deploy, { name: configuration.name, stage: options.stage || "prod", }); } } export function getCloudAdapter(provider) { switch (provider) { case CloudProviderIdentifier.GENEZIO_AWS: case CloudProviderIdentifier.GENEZIO_UNIKERNEL: return new GenezioCloudAdapter(); case CloudProviderIdentifier.GENEZIO_CLOUD: return new GenezioCloudAdapter(); case CloudProviderIdentifier.GENEZIO_CLUSTER: return new ClusterCloudAdapter(); default: throw new UserError(`Unsupported cloud provider: ${provider}`); } }