UNPKG

genezio

Version:

Command line utility to interact with Genezio infrastructure.

212 lines (205 loc) 10.5 kB
import path from "path"; import git from "isomorphic-git"; import fs from "fs"; import colors from "colors"; import { $ } from "execa"; import { debugLogger, log } from "../../../utils/logging.js"; import { actionDetectedEnvFile, attemptToInstallDependencies, prepareServicesPostBackendDeployment, prepareServicesPreBackendDeployment, readOrAskConfig, createBackendEnvVarList, uploadUserCode, } from "../utils.js"; import { addSSRComponentToConfig } from "../../analyze/utils.js"; import { NODE_DEFAULT_PACKAGE_MANAGER, PackageManagerType, } from "../../../packageManagers/packageManager.js"; import { DEFAULT_ARCHITECTURE, SSRFrameworkComponentType } from "../../../models/projectOptions.js"; import { UserError } from "../../../errors.js"; import { getCloudProvider } from "../../../requests/getCloudProvider.js"; import { functionToCloudInput, getCloudAdapter } from "../genezio.js"; import { FunctionType, Language } from "../../../projectConfiguration/yaml/models.js"; import { ProjectConfiguration } from "../../../models/projectConfiguration.js"; import { createTemporaryFolder } from "../../../utils/file.js"; import { DASHBOARD_URL } from "../../../constants.js"; import { warningMissingEnvironmentVariables } from "../../../utils/environmentVariables.js"; import { isCI } from "../../../utils/process.js"; export async function remixDeploy(options) { const genezioConfig = await readOrAskConfig(options.config); const packageManagerType = genezioConfig.remix?.packageManager || NODE_DEFAULT_PACKAGE_MANAGER; const cwd = process.cwd(); const componentPath = genezioConfig.remix?.path ? path.resolve(cwd, genezioConfig.remix.path) : cwd; // Give the user another chance if he forgot to add `--env` flag if (!isCI() && !options.env) { options.env = await actionDetectedEnvFile(componentPath, genezioConfig.name, options.stage); } // Prepare services before deploying (database, authentication, etc) await prepareServicesPreBackendDeployment(genezioConfig, genezioConfig.name, options.stage, options.env); // Install dependencies const installDependenciesCommand = await attemptToInstallDependencies([], componentPath, packageManagerType); // Check if the project uses remix vite by looking for vite.config.* files const isRemixVite = fs.existsSync(path.join(componentPath, "vite.config.ts")) || fs.existsSync(path.join(componentPath, "vite.config.js")) || fs.existsSync(path.join(componentPath, "vite.config.mjs")) || fs.existsSync(path.join(componentPath, "vite.config.cjs")); if (isRemixVite) { debugLogger.debug("Building the project using: remix vite:build"); // Build the project using remix vite await $({ stdio: "inherit", cwd: componentPath, }) `remix vite:build`.catch((error) => { throw new UserError(`Failed to build the Remix project. Check the logs above. ${error}`); }); } else { debugLogger.debug("Building the project using:remix build"); // Build the project using remix build await $({ stdio: "inherit", cwd: componentPath, }) `remix build`.catch((error) => { throw new UserError(`Failed to build the Remix project. Check the logs above. ${error}`); }); } const remixBuildCommand = isRemixVite ? "remix vite:build" : "remix build"; // Add remix component to config await addSSRComponentToConfig(options.config, { path: componentPath, packageManager: packageManagerType, scripts: { build: [remixBuildCommand], deploy: [`${installDependenciesCommand.command}`, remixBuildCommand], }, }, SSRFrameworkComponentType.remix); // Copy the build folder to /tmp const tempBuildCwd = await createTemporaryFolder(); debugLogger.debug(`Copying project files to ${tempBuildCwd}`); // Check and copy build folder const buildPath = path.join(componentPath, "build"); await fs.promises .cp(buildPath, tempBuildCwd, { recursive: true, force: true, dereference: true }) .catch((error) => { throw new UserError(`Failed to copy project build folder to temporary directory. ${error}`); }); if (!isRemixVite) { // Copy public folder const publicPath = path.join(componentPath, "public"); await fs.promises .mkdir(path.join(tempBuildCwd, "public"), { recursive: true }) .catch((error) => { throw new UserError(`Failed to create public folder in temporary directory. ${error}`); }); await fs.promises .cp(publicPath, path.join(tempBuildCwd, "public"), { recursive: true, force: true, dereference: true, }) .catch((error) => { throw new UserError(`Failed to copy public folder to temporary directory. ${error}`); }); } // Check and copy file package.json const packageJsonPath = path.join(componentPath, "package.json"); if (!fs.existsSync(packageJsonPath)) { throw new UserError("package.json not found in the project directory: " + packageJsonPath); } // Copy package.json to tempBuildCwd with proper destination path const destPackageJsonPath = path.join(tempBuildCwd, "package.json"); await fs.promises .cp(packageJsonPath, destPackageJsonPath, { recursive: true, force: true, dereference: true, }) .catch((error) => { throw new UserError(`Failed to copy package.json to temporary directory. ${error}`); }); if (isRemixVite) { const serverIndexPath = path.join(tempBuildCwd, "server", "index.js"); const serverIndexContent = await fs.promises.readFile(serverIndexPath, "utf-8"); const packageJsonPathServer = path.join(tempBuildCwd, "server", "package.json"); // Check if server/index.js use import syntax if use and dont have package.json with type: module create one const hasEsModules = /^[\s\n]*import\s+(?:[\w*\s{},]*\s+from\s+)?['"]/m.test(serverIndexContent); if (hasEsModules && !fs.existsSync(packageJsonPathServer)) { await fs.promises.writeFile(packageJsonPathServer, JSON.stringify({ type: "module" }, null, 2)); } } // Install express and @remix-run/express await attemptToInstallDependencies(["express", "@remix-run/express"], tempBuildCwd, packageManagerType); const serverMjsPath = path.join(tempBuildCwd, "server.mjs"); await fs.promises.writeFile(serverMjsPath, isRemixVite ? serverRemixViteContent : serverRemixClassicContent); const environmentVariables = await createBackendEnvVarList(options.env, options.stage, genezioConfig, SSRFrameworkComponentType.remix); const result = await deployFunction(genezioConfig, options, tempBuildCwd, environmentVariables); await uploadUserCode(genezioConfig.name, genezioConfig.region, options.stage, componentPath); const functionUrl = result.functions.find((f) => f.name === "function-remix")?.cloudUrl; await warningMissingEnvironmentVariables(genezioConfig.remix?.path || "./", result.projectId, result.projectEnvId); await prepareServicesPostBackendDeployment(genezioConfig, genezioConfig.name, options.stage); if (functionUrl) { log.info(`The app is being deployed at ${colors.cyan(functionUrl)}. It might take a few moments to be available worldwide.`); log.info(`\nApp Dashboard URL: ${colors.cyan(`${DASHBOARD_URL}/project/${result.projectId}/${result.projectEnvId}`)}\n` + `${colors.dim("Here you can monitor logs, set up a custom domain, and more.")}\n`); } else { log.warn("No deployment URL was returned."); } } async function deployFunction(config, options, cwd, environmentVariables) { const cloudProvider = await getCloudProvider(config.name); const cloudAdapter = getCloudAdapter(cloudProvider); const serverFunction = { path: ".", name: "remix", entry: "server.mjs", type: config.remix?.type === FunctionType.persistent ? FunctionType.persistent : FunctionType.httpServer, timeout: config.remix?.timeout, storageSize: config.remix?.storageSize, instanceSize: config.remix?.instanceSize, vcpuCount: config.remix?.vcpuCount, memoryMb: config.remix?.memoryMb, maxConcurrentRequestsPerInstance: config.remix?.maxConcurrentRequestsPerInstance, maxConcurrentInstances: config.remix?.maxConcurrentInstances, cooldownTime: config.remix?.cooldownTime, }; const deployConfig = { ...config, backend: { path: cwd, language: { name: Language.js, architecture: DEFAULT_ARCHITECTURE, packageManager: PackageManagerType.npm, ...(config.remix?.runtime !== undefined && { runtime: config.remix.runtime }), }, functions: [serverFunction], }, }; const projectConfiguration = new ProjectConfiguration(deployConfig, await getCloudProvider(deployConfig.name), { generatorResponses: [], classesInfo: [], }); const cloudInputs = await Promise.all(projectConfiguration.functions.map((f) => functionToCloudInput(f, cwd))); const projectGitRepositoryUrl = (await git.listRemotes({ fs, dir: process.cwd() })).find((r) => r.remote === "origin")?.url; const result = await cloudAdapter.deploy(cloudInputs, projectConfiguration, { stage: options.stage }, ["remix"], /* sourceRepository */ projectGitRepositoryUrl, /* environmentVariables */ environmentVariables); return result; } const serverRemixViteContent = ` import express from "express"; import { createRequestHandler } from "@remix-run/express"; import * as build from "./server/index.js"; const app = express(); app.use(express.static("client")); app.all("*", createRequestHandler({ build })); app.listen(8080, () => { console.log("Server is running on http://localhost:8080"); });`; const serverRemixClassicContent = ` import express from "express"; import { createRequestHandler } from "@remix-run/express"; import * as build from "./index.js"; const app = express(); app.use(express.static('public')); app.all('*', createRequestHandler({ build })); app.listen(8080, () => { console.log('Server running at http://localhost:8080'); });`;