genezio
Version:
Command line utility to interact with Genezio infrastructure.
1,076 lines • 64 kB
JavaScript
import express from "express";
import chokidar from "chokidar";
import cors from "cors";
import bodyParser from "body-parser";
import { spawn } from "child_process";
import path from "path";
import url from "url";
import colors from "colors";
import { createRequire } from "module";
import { ProjectConfiguration, } from "../models/projectConfiguration.js";
import { RECOMMENTDED_GENEZIO_TYPES_VERSION_RANGE, REQUIRED_GENEZIO_TYPES_VERSION_RANGE, } from "../constants.js";
import { GENEZIO_NO_CLASSES_FOUND, PORT_ALREADY_USED, UserError } from "../errors.js";
import { mapYamlClassToSdkClassConfiguration, sdkGeneratorApiHandler, } from "../generateSdk/generateSdkApi.js";
import { NodeJsLocalBundler } from "../bundlers/node/nodeJsLocalBundler.js";
import { BundlerComposer } from "../bundlers/bundlerComposer.js";
import { genezioRequestParser } from "../utils/genezioRequestParser.js";
import { debugLogger, doAdaptiveLogAction } from "../utils/logging.js";
import { rectifyCronString } from "../utils/rectifyCronString.js";
import cron from "node-cron";
import { createTemporaryFolder, fileExists, readUTF8File, writeToFile } from "../utils/file.js";
import { GenezioCommand, reportSuccess as _reportSuccess, reportSuccessFunctions, } from "../utils/reporter.js";
import { DartBundler } from "../bundlers/dart/localDartBundler.js";
import axios, { AxiosError } from "axios";
import { findAvailablePort } from "../utils/findAvailablePort.js";
import { DatabaseType, entryFileFunctionMap, FunctionType, Language, startingCommandMap, TriggerType, } from "../projectConfiguration/yaml/models.js";
import { YamlConfigurationIOController, } from "../projectConfiguration/yaml/v2.js";
import hash from "hash-it";
import { GenezioTelemetry, TelemetryEventTypes } from "../telemetry/telemetry.js";
import dotenv from "dotenv";
import { TsRequiredDepsBundler } from "../bundlers/node/typescriptRequiredDepsBundler.js";
import { DEFAULT_NODE_RUNTIME, SSRFrameworkComponentType, SSRFrameworkName, } from "../models/projectOptions.js";
import { exit } from "process";
import { log } from "../utils/logging.js";
import { interruptLocalPath } from "../utils/localInterrupt.js";
import { CloudProviderIdentifier, } from "../models/cloudProviderIdentifier.js";
import { LocalGoBundler } from "../bundlers/go/localGoBundler.js";
import { importServiceEnvVariables } from "../utils/servicesEnvVariables.js";
import { isDependencyVersionCompatible } from "../utils/jsProjectChecker.js";
import { scanClassesForDecorators } from "../utils/configuration.js";
import { runScript, runFrontendStartScript } from "../utils/scripts.js";
import { writeSdk } from "../generateSdk/sdkWriter/sdkWriter.js";
import { watchPackage } from "../generateSdk/sdkMonitor.js";
import { NodeJsBundler } from "../bundlers/node/nodeJsBundler.js";
import { KotlinBundler } from "../bundlers/kotlin/localKotlinBundler.js";
import { reportSuccessForSdk } from "../generateSdk/sdkSuccessReport.js";
import { Mutex } from "async-mutex";
import httpProxy from "http-proxy";
import * as readline from "readline";
import { getLinkedFrontendsForProject } from "../utils/linkDatabase.js";
import fsExtra from "fs-extra/esm";
import { enableAuthentication, EnvironmentResourceType, evaluateResource, getOrCreateDatabase, getOrCreateEmptyProject, hasInternetConnection, } from "./deploy/utils.js";
import { displayHint } from "../utils/strings.js";
import { enableEmailIntegration, getProjectIntegrations } from "../requests/integration.js";
import { expandEnvironmentVariables, findAnEnvFile } from "../utils/environmentVariables.js";
import { getFunctionHandlerProvider } from "../utils/getFunctionHandlerProvider.js";
import { getFunctionEntryFilename } from "../utils/getFunctionEntryFilename.js";
import fs from "fs";
import { detectPythonCommand } from "../utils/detectPythonCommand.js";
const httpServerPortMapping = {};
export async function prepareLocalBackendEnvironment(yamlProjectConfiguration, options) {
try {
const databases = yamlProjectConfiguration.services?.databases;
const authentication = yamlProjectConfiguration.services?.authentication;
const email = yamlProjectConfiguration.services?.email;
const backend = yamlProjectConfiguration.backend;
const frontend = yamlProjectConfiguration.frontend;
const projectName = yamlProjectConfiguration.name;
const region = yamlProjectConfiguration.region;
let configurationEnvVars = {};
if (yamlProjectConfiguration.services) {
if (!(await hasInternetConnection())) {
throw new UserError("No internet connection found. If you want to use services you need an active internet connection. Please check your internet connection and try again.");
}
const projectDetails = await getOrCreateEmptyProject(projectName, region, options.stage || "prod",
/* ask */ true);
if (databases && projectDetails) {
// Get connection URL and expose it as an environment variable only for the server process
for (const database of databases) {
if (!database.region) {
database.region = region;
}
let createdDatabaseRequest = {
name: database.name,
region: database.region,
type: database.type,
};
if (database.type === DatabaseType.mongo) {
createdDatabaseRequest = {
...createdDatabaseRequest,
clusterType: database.clusterType,
clusterName: database.clusterName,
clusterTier: database.clusterTier,
};
}
const remoteDatabase = await getOrCreateDatabase(createdDatabaseRequest, options.stage || "prod", projectDetails.projectId, projectDetails.projectEnvId,
/* ask */ true);
if (!remoteDatabase) {
break;
}
const databaseConnectionUrlKey = `${remoteDatabase.name.replace(/-/g, "_").toUpperCase()}_DATABASE_URL`;
configurationEnvVars = {
...configurationEnvVars,
[databaseConnectionUrlKey]: remoteDatabase.connectionUrl,
};
log.info(displayHint(`You can use \`process.env["${databaseConnectionUrlKey}"]\` to connect to the remote database \`${remoteDatabase.name}\`.`));
}
}
if (authentication && projectDetails) {
const envFile = options.env || (await findAnEnvFile(process.cwd()));
await enableAuthentication(yamlProjectConfiguration, projectDetails.projectId, projectDetails.projectEnvId, options.stage || "prod", envFile,
/* ask */ true);
log.info(displayHint(`You can reference authentication token and region using \${{services.authentication.token}} and \${{services.authentication.region}}.`));
}
if (email && projectDetails) {
const isEnabled = (await getProjectIntegrations(projectDetails.projectId, projectDetails.projectEnvId)).integrations.find((integration) => integration === "EMAIL-SERVICE");
if (!isEnabled) {
await enableEmailIntegration(projectDetails.projectId, projectDetails.projectEnvId);
log.info("Email integration enabled successfully.");
}
log.info(displayHint(`You can use \`process.env[EMAIL_SERVICE_TOKEN]\` to send emails.`));
}
}
if (!backend) {
throw new UserError("No backend component found in the genezio.yaml file.");
}
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 sdkLanguages = [];
// Add configuration frontends that contain the SDK field
sdkLanguages.push(...(frontend || [])
.map((f) => f.sdk?.language)
.filter((f) => f !== undefined));
// Add linked frontends
sdkLanguages.push(...(await getLinkedFrontendsForProject(yamlProjectConfiguration.name)).map((f) => f.language));
const sdkResponse = await sdkGeneratorApiHandler(sdkLanguages, mapYamlClassToSdkClassConfiguration(backend.classes, backend.language.name, backend.path), backend.path,
/* packageName= */ `@genezio-sdk/${yamlProjectConfiguration.name}`).catch((error) => {
if (error.code === "ENOENT") {
log.error(`The file ${error.path} does not exist. Please check your genezio.yaml configuration and make sure that all the file paths are correct.`);
}
throw error;
});
const projectConfiguration = new ProjectConfiguration(yamlProjectConfiguration, CloudProviderIdentifier.GENEZIO_AWS, sdkResponse);
const processForLocalUnits = await startProcesses(backend, projectConfiguration, sdkResponse, options, configurationEnvVars);
return await new Promise((resolve) => {
resolve({
restartEnvironment: false,
spawnOutput: {
success: true,
projectConfiguration,
processForLocalUnits,
sdk: sdkResponse,
},
});
});
}
catch (error) {
if (error instanceof Error) {
log.error(error.message);
}
log.error(`Fix the errors and genezio local will restart automatically. Waiting for changes...`);
// If there was an error generating the SDK, wait for changes and try again.
const { watcher } = await listenForChanges();
logChangeDetection();
return new Promise((resolve) => {
resolve({
restartEnvironment: true,
watcher,
});
});
}
}
// Function that starts the local environment. It starts the backend watcher and the frontends.
export async function startLocalEnvironment(options) {
log.settings.prettyLogTemplate = `${colors.blue("|")} `;
await GenezioTelemetry.sendEvent({
eventType: TelemetryEventTypes.GENEZIO_LOCAL,
commandOptions: JSON.stringify(options),
});
const yamlConfigIOController = new YamlConfigurationIOController(options.config);
const yamlProjectConfiguration = await yamlConfigIOController.read();
// This mutex is used to make the frontends wait until the first Genezio SDK is generated.
const sdkSynchronizer = new Mutex();
// It is locked until the first Genezio SDK is generated.
sdkSynchronizer.acquire();
if (!yamlProjectConfiguration.backend &&
!yamlProjectConfiguration.frontend &&
!yamlProjectConfiguration.nextjs &&
!yamlProjectConfiguration.nuxt &&
!yamlProjectConfiguration.nestjs &&
!yamlProjectConfiguration.nitro &&
!yamlProjectConfiguration.remix &&
!yamlProjectConfiguration.streamlit) {
throw new UserError("No backend or frontend components found in the genezio.yaml file. You need at least one component to start the local environment.");
}
if (!yamlProjectConfiguration.backend &&
yamlProjectConfiguration.frontend &&
yamlProjectConfiguration.frontend.every((f) => !f.scripts?.start)) {
throw new UserError("No start script found for any frontend component. You need at least one start script to start the local environment.");
}
const ssrFrameworks = Object.values(SSRFrameworkComponentType)
.map((frameworkType) => ({
config: yamlProjectConfiguration[frameworkType],
name: frameworkType,
}))
.filter((framework) => framework.config);
// Start all components in parallel
await Promise.all([
startBackendWatcher(yamlProjectConfiguration.backend, options, sdkSynchronizer),
startFrontends(yamlProjectConfiguration.frontend, sdkSynchronizer, yamlProjectConfiguration, options.stage || "prod", options.port),
...ssrFrameworks.map((framework) => startSsrFramework(framework.config, framework.name, yamlProjectConfiguration, options.stage || "prod", options.port, options.env)),
]);
}
/**
* Starts the frontends based on the provided configuration.
*
* @param frontendConfiguration - The configuration for the frontends.
* @param sdkSynchronizer - The mutex used for synchronizing the SDK generation.
* @returns Never returns, because it runs the frontends indefinitely.
*/
async function startFrontends(frontendConfiguration, sdkSynchronizer, configuration, stage, port) {
if (!frontendConfiguration)
return;
// Start the frontends only after the first Genezio SDK was generated, until then wait.
await sdkSynchronizer.waitForUnlock();
await Promise.all(frontendConfiguration.map(async (frontend) => {
const newEnvObject = await expandEnvironmentVariables(frontend.environment, configuration, stage,
/* envFile */ undefined, {
isLocal: true,
port: port,
isFrontend: true,
});
debugLogger.debug(`Environment variables injected for frontend.scripts.local:`, JSON.stringify(newEnvObject));
await runFrontendStartScript(frontend.scripts?.start, frontend.path, newEnvObject).catch((e) => log.error(new Error(`Failed to start frontend located in \`${frontend.path}\`: ${e.message}`)));
}));
}
/**
* Starts the backend watcher for local development.
*
* @param backendConfiguration - The backend configuration.
* @param options - The Genezio local options.
* @param sdkSynchronizer - The mutex for synchronizing SDK generation.
* @returns Never returns, because it runs the local environment indefinitely.
*/
async function startBackendWatcher(backendConfiguration, options, sdkSynchronizer) {
if (!backendConfiguration) {
sdkSynchronizer.release();
return;
}
// 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 (backendConfiguration.language.name === Language.ts ||
backendConfiguration.language.name === Language.js) {
const packageJsonPath = path.join(backendConfiguration.path, "package.json");
if (isDependencyVersionCompatible(packageJsonPath, "@genezio/types", REQUIRED_GENEZIO_TYPES_VERSION_RANGE) === false) {
log.error(`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}`);
exit(1);
}
}
await doAdaptiveLogAction("Running backend local scripts", async () => {
await runScript(backendConfiguration.scripts?.local, backendConfiguration.path);
}).catch(async (error) => {
await GenezioTelemetry.sendEvent({
eventType: TelemetryEventTypes.GENEZIO_PRE_START_LOCAL_SCRIPT_ERROR,
commandOptions: JSON.stringify(options),
});
throw error;
});
// Check if a deployment is in progress and if it is, stop the local environment
chokidar.watch(interruptLocalPath, { ignoreInitial: true }).on("all", async () => {
log.info("A deployment is in progress. Stopping local environment...");
exit(0);
});
// eslint-disable-next-line no-constant-condition
while (true) {
// Read the project configuration every time because it might change
let yamlProjectConfiguration;
try {
yamlProjectConfiguration = await new YamlConfigurationIOController(options.config).read();
}
catch (error) {
if (error instanceof Error) {
log.error(error.message);
}
log.error(`Fix the errors and genezio local will restart automatically. Waiting for changes...`);
// If there was an error while parsing using babel the decorated class, wait for changes and try again.
const { watcher } = await listenForChanges();
if (watcher) {
watcher.close();
}
logChangeDetection();
continue;
}
const listenForChangesPromise = listenForChanges();
const localUnitProcessSpawnPromise = prepareLocalBackendEnvironment(yamlProjectConfiguration, options);
let promiseRes = await Promise.race([
localUnitProcessSpawnPromise,
listenForChangesPromise,
]);
// If the listenForChanges promise is resolved first, it means that the user made a change in the code and we
// need to rebundle and restart the backend.
if (promiseRes.restartEnvironment === true) {
// Wait for classes to be spawned before restarting the environment
promiseRes = await localUnitProcessSpawnPromise;
if (!promiseRes.spawnOutput || promiseRes.spawnOutput.success === false) {
continue;
}
// clean up the old processes
promiseRes.spawnOutput.processForLocalUnits.forEach((unitProcess) => {
unitProcess.process.kill();
});
if (promiseRes.watcher) {
promiseRes.watcher.close();
}
logChangeDetection();
continue;
}
if (!promiseRes.spawnOutput || promiseRes.spawnOutput.success === false) {
continue;
}
const projectConfiguration = promiseRes.spawnOutput.projectConfiguration;
const processForUnits = promiseRes.spawnOutput.processForLocalUnits;
const sdk = promiseRes.spawnOutput.sdk;
// Start HTTP Server
const server = await startServerHttp(options.port, yamlProjectConfiguration.name, processForUnits, projectConfiguration);
// Start cron jobs
const crons = await startCronJobs(projectConfiguration, processForUnits, yamlProjectConfiguration, options.port);
log.info("\x1b[36m%s\x1b[0m", "Your local server is running and the SDK was successfully generated!");
const watcherTimeouts = await handleSdk(yamlProjectConfiguration.name, yamlProjectConfiguration.frontend, sdk, options);
reportSuccess(projectConfiguration, options.port);
if (sdkSynchronizer.isLocked())
sdkSynchronizer.release();
// This check makes sense only for js/ts backend, skip for dart, go etc.
if (backendConfiguration.language.name === Language.ts ||
backendConfiguration.language.name === Language.js) {
const nodeVersion = (projectConfiguration.options && "nodeRuntime" in projectConfiguration.options
? projectConfiguration.options.nodeRuntime
: undefined) || DEFAULT_NODE_RUNTIME;
reportDifferentNodeRuntime(nodeVersion);
}
// Start listening for changes in user's code
const { watcher } = await listenForChanges();
if (watcher) {
watcher.close();
}
logChangeDetection();
// When new changes are detected, close everything and restart the process
clearAllResources(server, processForUnits, crons);
await GenezioTelemetry.sendEvent({
eventType: TelemetryEventTypes.GENEZIO_LOCAL_RELOAD,
commandOptions: JSON.stringify(options),
});
watcherTimeouts.forEach((timeout) => clearTimeout(timeout));
}
}
function logChangeDetection() {
log.info("\x1b[36m%s\x1b[0m", "Change detected, reloading...");
}
/**
* Bundle each class and start a new process for it.
*/
async function startProcesses(backend, projectConfiguration, sdk, options, configurationEnvVars) {
const classes = projectConfiguration.classes;
const processForLocalUnits = new Map();
// Bundle each class and start a new process for it
const bundlersOutputPromiseClasses = classes.map(async (classInfo) => {
const bundler = getBundler(classInfo);
if (!bundler) {
throw new UserError(`Unsupported language ${classInfo.language}.`);
}
const astClass = sdk.classesInfo.find((c) => c.classConfiguration.path === classInfo.path);
if (astClass === undefined) {
throw new UserError("AST class not found.");
}
const ast = astClass.program;
debugLogger.debug("Start bundling...");
const tmpFolder = await createTemporaryFolder(`${classInfo.name}-${hash(classInfo.path)}`);
const bundlerOutput = await bundler.bundle({
projectConfiguration,
path: classInfo.path,
ast: ast,
genezioConfigurationFilePath: process.cwd(),
configuration: classInfo,
extra: {
mode: "development",
tmpFolder: tmpFolder,
installDeps: options.installDeps,
},
});
return { ...bundlerOutput, type: "class" };
});
const bundlersOutputPromiseFunctions = projectConfiguration.functions?.map(async (functionInfo) => {
const tmpFolder = await createTemporaryFolder(`${functionInfo.name}-${hash(functionInfo.path)}`);
// delete all content in the tmp folder
await fsExtra.emptyDir(tmpFolder);
if (functionInfo.language === Language.python ||
functionInfo.language === Language.pythonAsgi) {
await fsExtra.copy(backend.path, tmpFolder);
}
else {
await fsExtra.copy(path.join(backend.path, functionInfo.path), tmpFolder);
}
const handlerProvider = getFunctionHandlerProvider(functionInfo.type, functionInfo.language);
// if handlerProvider is Http, run it with node
if ((functionInfo.type === FunctionType.httpServer ||
functionInfo.type === FunctionType.persistent) &&
(functionInfo.language === Language.js || functionInfo.language === Language.ts)) {
await writeToFile(path.join(tmpFolder), getFunctionEntryFilename(functionInfo.language, "local_function_wrapper"), await getLocalFunctionHttpServerWrapper(functionInfo.entry));
}
// if handlerProvider is Http and language is python
else if ((functionInfo.type === FunctionType.httpServer ||
functionInfo.type === FunctionType.persistent) &&
(functionInfo.language === Language.python ||
functionInfo.language === Language.pythonAsgi)) {
await writeToFile(path.join(tmpFolder), getFunctionEntryFilename(functionInfo.language, "local_function_wrapper"), await getLocalFunctionHttpServerPythonWrapper(functionInfo.path, functionInfo.entry, functionInfo.handler));
}
else {
await writeToFile(path.join(tmpFolder), getFunctionEntryFilename(functionInfo.language, "local_function_wrapper"), await handlerProvider.getLocalFunctionWrapperCode(functionInfo.handler, functionInfo));
}
return {
configuration: functionInfo,
extra: {
type: "function",
startingCommand: startingCommandMap[functionInfo.language],
commandParameters: [
path.resolve(tmpFolder, `local_function_wrapper.${entryFileFunctionMap[functionInfo.language].split(".")[1]}`),
],
},
};
});
const bundlersOutput = await Promise.all([
...bundlersOutputPromiseClasses,
...bundlersOutputPromiseFunctions,
]);
try {
await importServiceEnvVariables(projectConfiguration.name, projectConfiguration.region, options.stage ? options.stage : "prod");
}
catch (error) {
if (error instanceof UserError) {
throw error;
}
}
const envVars = {};
const envFile = projectConfiguration.workspace?.backend
? path.join(projectConfiguration.workspace.backend, ".env")
: path.join(process.cwd(), ".env");
dotenv.config({ path: options.env || envFile, processEnv: envVars });
for (const bundlerOutput of bundlersOutput) {
const extra = bundlerOutput.extra;
if (!extra) {
throw new UserError("Bundler output is missing extra field.");
}
if (!extra.startingCommand) {
throw new UserError("No starting command found for this language.");
}
await startLocalUnitProcess(extra.startingCommand, extra.commandParameters ? extra.commandParameters : [], bundlerOutput.configuration.name, processForLocalUnits, envVars, extra.type || "class", projectConfiguration.workspace?.backend, configurationEnvVars);
}
return processForLocalUnits;
}
// Function that returns the correct bundler for the local environment based on language.
function getBundler(classConfiguration) {
let bundler;
switch (classConfiguration.language) {
case "ts": {
const requiredDepsBundler = new TsRequiredDepsBundler();
const nodeJsBundler = new NodeJsBundler();
const localBundler = new NodeJsLocalBundler();
bundler = new BundlerComposer([requiredDepsBundler, nodeJsBundler, localBundler]);
break;
}
case "js": {
const nodeJsBundler = new NodeJsBundler();
const localBundler = new NodeJsLocalBundler();
bundler = new BundlerComposer([nodeJsBundler, localBundler]);
break;
}
case "dart": {
bundler = new DartBundler();
break;
}
case "kt": {
bundler = new KotlinBundler();
break;
}
case "go": {
bundler = new LocalGoBundler();
break;
}
default: {
log.error(`Unsupported language ${classConfiguration.language}. Skipping class `);
}
}
return bundler;
}
async function startServerHttp(port, projectName, processForUnits, projectConfiguration) {
const astSummary = projectConfiguration.astSummary;
const app = express();
const require = createRequire(import.meta.url);
app.use(cors({
origin: "*",
methods: "GET, POST, OPTIONS, PUT, PATCH, DELETE",
allowedHeaders: "*",
}));
app.use(bodyParser.raw({ type: () => true, limit: "6mb" }));
app.use(genezioRequestParser);
const packagePath = path.dirname(require.resolve("@genezio/test-interface-component"));
// serve test interface built folder on localhost
const buildFolder = path.join(packagePath, "build");
app.use(express.static(buildFolder));
app.get(`/explore`, (_req, res) => {
const filePath = path.join(buildFolder, "index.html");
res.sendFile(filePath);
});
app.get("/get-ast-summary", (_req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ...astSummary, name: projectName }));
});
app.get("/get-functions", (_req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
functions: getProjectFunctions(port, projectConfiguration),
name: projectName,
}));
});
app.all(`/:className`, async (req, res) => {
const reqToFunction = getEventObjectFromRequest(req);
const localProcess = processForUnits.get(req.params.className);
if (!localProcess) {
sendResponse(res, {
body: JSON.stringify({
jsonrpc: "2.0",
id: 0,
error: { code: -32000, message: "Class not found!" },
}),
isBase64Encoded: false,
statusCode: "200",
statusDescription: "200 OK",
headers: {
"content-type": "application/json",
},
cookies: [],
});
return;
}
try {
const response = await communicateWithProcess(localProcess, req.params.className, reqToFunction, processForUnits);
sendResponse(res, response.data);
}
catch (error) {
sendResponse(res, {
body: JSON.stringify({
jsonrpc: "2.0",
id: 0,
error: { code: -32000, message: "Internal error" },
}),
isBase64Encoded: false,
statusCode: "200",
statusDescription: "200 OK",
headers: {
"content-type": "application/json",
},
cookies: [],
});
return;
}
});
async function handlerFunctionCall(req, res) {
const reqToFunction = getEventObjectFromRequest(req);
// remove /.functions/:functionName from the url in order to get expected path for the function
reqToFunction.rawPath = "/" + reqToFunction.rawPath?.split("/").slice(3).join("/");
reqToFunction.requestContext.http.path = reqToFunction.rawPath;
const localProcess = processForUnits.get(req.params.functionName);
if (!localProcess) {
sendResponse(res, {
body: "Function not found!",
isBase64Encoded: false,
statusCode: "500",
statusDescription: "500 Internal Server Error",
headers: {
"content-type": "application/json",
},
cookies: [],
});
return;
}
try {
const response = await communicateWithProcess(localProcess, req.params.functionName, reqToFunction, processForUnits);
sendResponse(res, response.data);
}
catch (error) {
sendResponse(res, {
body: JSON.stringify({ message: "Internal server error", error: error }),
isBase64Encoded: false,
statusCode: "500",
statusDescription: "500 Internal Server Error",
headers: {
"content-type": "application/json",
},
cookies: [],
});
return;
}
}
app.all(`/.functions/:functionName/*`, async (req, res) => {
await handlerFunctionCall(req, res);
});
app.all(`/.functions/:functionName`, async (req, res) => {
await handlerFunctionCall(req, res);
});
async function handlerHttpMethod(req, res) {
const reqToFunction = getEventObjectFromRequest(req);
const localProcess = processForUnits.get(req.params.className);
if (!localProcess) {
res.status(404).send(`Class ${req.params.className} not found.`);
return;
}
try {
const response = await communicateWithProcess(localProcess, req.params.className, reqToFunction, processForUnits);
sendResponse(res, response.data);
}
catch (error) {
sendResponse(res, {
body: JSON.stringify({
error: { code: -32000, message: "Internal error" },
}),
isBase64Encoded: false,
statusCode: "500",
statusDescription: "Internal Server Error",
headers: {
"content-type": "application/json",
},
cookies: [],
});
return;
}
}
app.all(`/:className/:methodName`, async (req, res) => {
await handlerHttpMethod(req, res);
});
app.all(`/:className/:methodName/*`, async (req, res) => {
await handlerHttpMethod(req, res);
});
return await new Promise((resolve, reject) => {
const server = app.listen(port, "0.0.0.0", () => {
log.info(`Server listening on port ${port}`);
resolve(server);
});
server.on("error", (error) => {
const err = error;
if (err.code === "EADDRINUSE") {
reject(new UserError(PORT_ALREADY_USED(port)));
}
reject(error);
});
// this is needed to handle the websocket connections
server.on("upgrade", (req, socket, head) => {
if (req.url === undefined) {
return;
}
const parsedURL = url.parse(req.url, true);
const localProcess = processForUnits.get(parsedURL.query["class"]);
const proxy = httpProxy.createProxyServer({
target: {
host: "127.0.0.1",
port: localProcess?.listeningPort || 8080,
},
ws: true,
});
try {
proxy.ws(req, socket, head);
}
catch (error) {
throw new UserError("Error while upgrading the connection to websocket.");
}
});
});
}
function getProjectFunctions(port, projectConfiguration) {
return projectConfiguration.functions.map((f) => ({
cloudUrl: retrieveLocalFunctionUrl(f.name, f.type),
id: f.name,
name: f.name,
}));
}
async function startCronJobs(projectConfiguration, processForUnits, yamlProjectConfiguration, port) {
const cronHandlers = [];
for (const classElement of projectConfiguration.classes) {
const methods = classElement.methods;
for (const method of methods) {
if (method.type === TriggerType.cron && method.cronString) {
const cronHandler = {
cronString: rectifyCronString(method.cronString),
cronObject: null,
};
const process = processForUnits.get(classElement.name);
cronHandler.cronObject = cron.schedule(cronHandler.cronString, () => {
const reqToFunction = {
genezioEventType: "cron",
methodName: method.name,
cronString: cronHandler.cronString,
};
void communicateWithProcess(process, classElement.name, reqToFunction, processForUnits);
});
cronHandler.cronObject.start();
cronHandlers.push(cronHandler);
}
}
}
if (yamlProjectConfiguration.services && yamlProjectConfiguration.services.crons) {
for (const cronService of yamlProjectConfiguration.services.crons) {
const functionName = await evaluateResource(yamlProjectConfiguration, [
EnvironmentResourceType.RemoteResourceReference,
EnvironmentResourceType.LiteralValue,
], cronService.function, undefined, undefined, {
isLocal: true,
port: port,
});
const endpoint = cronService.endpoint?.replace(/^\//, "");
const cronString = cronService.schedule;
const functionConfiguration = projectConfiguration.functions.find((f) => f.name === `function-${functionName}`);
if (!functionConfiguration) {
throw new UserError(`Function ${functionName} not found in deployed functions. Check if your function is deployed. If the problem persists, please contact support at contact@genez.io.`);
}
const baseURL = retrieveLocalFunctionUrl(functionConfiguration.name, functionConfiguration.type);
let url;
if (endpoint) {
url = `${baseURL}/${endpoint}`;
}
else {
url = baseURL;
}
const cronHandler = {
cronString: rectifyCronString(cronString),
cronObject: null,
};
cronHandler.cronObject = cron.schedule(cronHandler.cronString, async () => {
log.info("DEBUG: trigger cron: " +
cronHandler.cronString +
" on function " +
functionName);
await axios.post(url);
});
cronHandler.cronObject.start();
cronHandlers.push(cronHandler);
}
}
return cronHandlers;
}
async function stopCronJobs(cronHandlers) {
for (const cronHandler of cronHandlers) {
if (cronHandler.cronObject) {
cronHandler.cronObject.stop();
}
}
}
function getEventObjectFromRequest(request) {
const urlDetails = url.parse(request.url, true);
const date = new Date();
return {
version: "2.0",
routeKey: "$default",
rawPath: urlDetails.pathname,
headers: request.headers,
rawQueryString: urlDetails.search ? urlDetails.search?.slice(1) : "",
queryStringParameters: urlDetails.search ? Object.assign({}, urlDetails.query) : undefined,
body: request.body,
isBase64Encoded: request.isBase64Encoded,
requestContext: {
http: {
method: request.method,
path: urlDetails.pathname,
protocol: request.httpVersion,
sourceIp: request.socket.remoteAddress,
userAgent: request.headers["user-agent"],
},
accountId: "anonymous",
apiId: "localhost",
domainName: "localhost",
domainPrefix: "localhost",
requestId: "undefined",
routeKey: "$default",
stage: "$default",
time: formatTimestamp(date),
timeEpoch: Date.now(),
},
};
}
function sendResponse(res, httpResponse) {
if (httpResponse.statusDescription) {
res.statusMessage = httpResponse.statusDescription;
}
let contentTypeHeader = false;
if (httpResponse.headers) {
for (const header of Object.keys(httpResponse.headers)) {
const headerContent = httpResponse.headers[header];
if (headerContent !== undefined) {
res.setHeader(header.toLowerCase(), headerContent);
}
if (header.toLowerCase() === "content-type") {
contentTypeHeader = true;
}
}
}
if (!contentTypeHeader) {
res.setHeader("content-type", "application/json");
}
if (httpResponse.cookies) {
for (const cookie of httpResponse.cookies) {
res.setHeader("Set-Cookie", cookie);
}
}
if (httpResponse.statusCode) {
res.writeHead(parseInt(httpResponse.statusCode));
}
if (httpResponse.isBase64Encoded === true) {
res.end(Buffer.from(httpResponse.body, "base64"));
}
else {
if (Buffer.isBuffer(httpResponse.body)) {
res.end(JSON.stringify(httpResponse.body.toJSON()));
}
else if (typeof httpResponse.body === "object") {
res.end(JSON.stringify(httpResponse.body));
}
else {
res.end(httpResponse.body ? httpResponse.body.toString() : "");
}
}
}
async function listenForChanges() {
const cwd = process.cwd();
let ignoredPathsFromGenezioIgnore = [];
// check for .genezioignore file
const ignoreFilePath = path.join(cwd, ".genezioignore");
if (await fileExists(ignoreFilePath)) {
// read the file as a string
const ignoreFile = await readUTF8File(ignoreFilePath);
// split the string by newline - CRLF or LF
const ignoreFileLines = ignoreFile.split(/\r?\n/);
// remove empty lines
const ignoreFileLinesWithoutEmptyLines = ignoreFileLines.filter((line) => line !== "" && !line.startsWith("#"));
ignoredPathsFromGenezioIgnore = ignoreFileLinesWithoutEmptyLines.map((line) => {
if (line.startsWith("/")) {
return line;
}
return path.join(cwd, line);
});
}
return new Promise((resolve) => {
// Watch for changes in the classes and update the handlers
const watchPaths = [path.join(cwd, "/**/*")];
const ignoredPaths = ["**/node_modules/*", ...ignoredPathsFromGenezioIgnore];
const watch = chokidar
.watch(watchPaths, {
// Disable fsevents for macos
useFsEvents: false,
ignored: ignoredPaths,
ignoreInitial: true,
})
.on("all", async () => {
resolve({
restartEnvironment: true,
watcher: watch,
});
});
});
}
/**
* Handles the SDK generation and the SDK writing, the SDK publishing to registries.
* It also monitors the SDK for changes and updates the local SDKs.
*
* @param projectName The name of the project.
* @param projectRegion The region of the project.
* @param frontend The frontend configuration.
* @param backendSdk The backend SDK configuration.
* @param sdk The SDK response.
* @param options The local options.
*
* @returns NodeJS.Timeout that can be used to stop the watchers.
*/
async function handleSdk(projectName, frontends, sdk, options) {
const nodeJsWatchers = [];
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(projectName);
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 workspaceUrl = getWorkspaceUrl(options.port);
const classUrls = sdkResponse.files.map((c) => ({
name: c.className,
cloudUrl: workspaceUrl
? `${workspaceUrl}/${c.className}`
: `http://127.0.0.1:${options.port}/${c.className}`,
}));
const sdkFolderPath = await writeSdk({
language: sdkLocation.language,
packageName: `@genezio-sdk/${projectName}`,
packageVersion: undefined,
sdkResponse,
classUrls,
publish: false,
installPackage: true,
outputPath: sdkLocation.path,
});
debugLogger.debug(`SDK for ${sdkLocation.language} written in ${sdkLocation.path}. ${sdkFolderPath}`);
if (sdkFolderPath) {
const timeout = await watchPackage(sdkLocation.language, projectName, frontends?.filter((f) => f.sdk?.language === Language.ts || f.sdk?.language === Language.js), sdkFolderPath);
if (timeout) {
nodeJsWatchers.push(timeout);
}
}
reportSuccessForSdk(sdkLocation.language, sdkResponse, GenezioCommand.local, {
name: projectName,
stage: "local",
});
}
return nodeJsWatchers;
}
function getWorkspaceUrl(port) {
let workspaceUrl;
if (process.env?.["GITPOD_WORKSPACE_URL"]) {
const gitPodWorkspaceUrl = process.env["GITPOD_WORKSPACE_URL"];
workspaceUrl = gitPodWorkspaceUrl.replace("https://", `https://${port}-`);
}
if (process.env?.["CODESPACE_NAME"] &&
process.env?.["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"]) {
const codespaceName = process.env["CODESPACE_NAME"];
const portForwardingDomain = process.env["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"];
workspaceUrl = `https://${codespaceName}-${port}.${portForwardingDomain}`;
}
return workspaceUrl;
}
function reportSuccess(projectConfiguration, port) {
const classesInfo = projectConfiguration.classes.map((c) => ({
className: c.name,
methods: c.methods.map((m) => ({
name: m.name,
type: m.type,
cronString: m.cronString,
functionUrl: getFunctionUrl(`http://127.0.0.1:${port}`, m.type, c.name, m.name),
})),
functionUrl: `http://127.0.0.1:${port}/${c.name}`,
}));
_reportSuccess(classesInfo);
if (projectConfiguration.functions?.length > 0) {
reportSuccessFunctions(projectConfiguration.functions.map((f) => ({
name: f.name,
id: f.name,
cloudUrl: retrieveLocalFunctionUrl(f.name, f.type),
})));
}
const workspaceUrl = getWorkspaceUrl(port);
if (projectConfiguration.classes.length > 0) {
log.info(colors.cyan(`Test your classes at ${workspaceUrl ? workspaceUrl : `http://localhost:${port}`}/explore`));
}
}
// This method is used to check if the user has a different node version installed than the one used by the server.
// If the user has a different version, a warning message will be displayed.
function reportDifferentNodeRuntime(userDefinedNodeRuntime) {
const installedNodeVersion = process.version;
// get server used version
let serverNodeRuntime = DEFAULT_NODE_RUNTIME;
if (userDefinedNodeRuntime) {
serverNodeRuntime = userDefinedNodeRuntime;
}
const nodeMajorVersion = installedNodeVersion.split(".")[0].slice(1);
const serverMajorVersion = serverNodeRuntime.split(".")[0].split("nodejs")[1];
// check if server version is different from installed version
if (nodeMajorVersion !== serverMajorVersion) {
log.warn(`${colors.yellow(`Warning: The installed node version ${installedNodeVersion} but your server is configured to use ${serverNodeRuntime}. This might cause unexpected behavior.
To change the server version, go to your ${colors.cyan("genezio.yaml")} file and change the ${colors.cyan("backend.language.runtime")} property to the version you want to use.`)}`);
}
}
function getFunctionUrl(baseUrl, methodType, className, methodName) {
if (methodType === "http") {
return `${baseUrl}/${className}/${methodName}`;
}
else {
return `${baseUrl}/${className}`;
}
}
async function clearAllResources(server, processForUnits, crons) {
process.env["LOGGED_IN_LOCAL"] = "";
// await for server.close();
await new Promise((resolve) => {
server.close(() => {
resolve(true);
});
});
await stopCronJobs(crons);
processForUnits.forEach((unitProcess) => {
unitProcess.process.kill();
});
}
async function startLocalUnitProcess(startingCommand, parameters, localUnitName, processForUnits, envVars = {}, type, cwd, configurationEnvVars) {
// Load .env file from the current working directory or specified cwd
const envPath = path.join(cwd || process.cwd(), ".env");
const envConfig = dotenv.config({ path: envPath });
const loadedEnvVars = envConfig.parsed || {};
const modifyLocalUnitName = localUnitName.replace(/-/g, "_").toUpperCase();
const portEnvKey = `GENEZIO_PORT_${modifyLocalUnitName}`;
// Check for existing port in environment variables
let availablePort;
const existingPort = loadedEnvVars[portEnvKey] || process.env[portEnvKey];
if (existingPort) {
availablePort = parseInt(existingPort, 10);
}
else if (type === "function" && httpServerPortMapping[localUnitName]) {
availablePort = httpServerPortMapping[localUnitName];
}
else {
availablePort = await findAvailablePort();
if (type === "function") {
httpServerPortMapping[localUnitName] = availablePort;
}
}
debugLogger.debug(`[START_Unit_PROCESS] Starting ${localUnitName} on port ${availablePort}`);
debugLogger.debug(`[START_Unit_PROCESS] Starting command: ${startingCommand}`);
debugLogger.debug(`[START_Unit_PROCESS] Parameters: ${parameters}`);
if (!process.env[portEnvKey]) {
process.env[portEnvKey] = availablePort.toString();
}
const processParameters = [...parameters, availablePort.toString()];
const localUnitProcess = spawn(startingCommand, processParameters, {
stdio: ["pipe", "pipe", "pipe"],
env: {
...loadedEnvVars,
...process.env,
...envVars,
...configurationEnvVars,
NODE_OPTIONS: "--enable-source-maps",
},
cwd,
});
const localUnitStdoutLineStream = readline.createInterface({
input: localUnitProcess.stdout,
});
const localUnitStderrLineStream = readline.createInterface({
input: localUnitProcess.stderr,
});
localUnitStdoutLineStream.on("line", (line) => log.info(line));
localUnitStderrLineStream.on("line", (line) => log.info(line));
processForUnits.set(localUnitName, {
type: type,
process: localUnitProcess,
listeningPort: availablePort,
startingCommand: startingCommand,
parameters: parameters,
envVars: envVars,
});
}
async function communicateWithProcess(localProcess, unitName, data, processForUnits) {
try {
return await axios.post(`http://127.0.0.1:${localProcess?.listeningPort}`, data);
}
catch (error) {
if (error instanceof AxiosError &&
(error.code === "ECONNRESET" || error.code