convex
Version:
Client for the Convex Cloud
528 lines (527 loc) • 17.9 kB
JavaScript
;
import chalk from "chalk";
import {
logFailure,
logFinishedStep,
logMessage,
logWarning,
showSpinner
} from "../bundler/context.js";
import {
fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows,
createProject,
loadSelectedDeploymentCredentials,
checkAccessToSelectedProject,
validateDeploymentSelectionForExistingDeployment
} from "./lib/api.js";
import {
configFilepath,
configName,
readProjectConfig,
upgradeOldAuthInfoToAuthConfig,
writeProjectConfig
} from "./lib/config.js";
import {
eraseDeploymentEnvVar,
writeDeploymentEnvVar
} from "./lib/deployment.js";
import { finalizeConfiguration } from "./lib/init.js";
import {
CONVEX_DEPLOYMENT_ENV_VAR_NAME,
functionsDir,
hasProjects,
logAndHandleFetchError,
selectDevDeploymentType,
validateOrSelectProject,
validateOrSelectTeam
} from "./lib/utils/utils.js";
import { writeConvexUrlToEnvFile } from "./lib/envvars.js";
import path from "path";
import { projectDashboardUrl } from "./lib/dashboard.js";
import { doCodegen, doInitCodegen } from "./lib/codegen.js";
import { handleLocalDeployment } from "./lib/localDeployment/localDeployment.js";
import {
promptOptions,
promptString,
promptYesNo
} from "./lib/utils/prompts.js";
import { readGlobalConfig } from "./lib/utils/globalConfig.js";
import {
deploymentNameFromSelection,
shouldAllowAnonymousDevelopment
} from "./lib/deploymentSelection.js";
import { ensureLoggedIn } from "./lib/login.js";
import { handleAnonymousDeployment } from "./lib/localDeployment/anonymous.js";
export async function deploymentCredentialsOrConfigure(ctx, deploymentSelection, chosenConfiguration, cmdOptions, partitionId) {
const selectedDeployment = await _deploymentCredentialsOrConfigure(
ctx,
deploymentSelection,
chosenConfiguration,
cmdOptions,
partitionId
);
if (selectedDeployment.deploymentFields !== null) {
await updateEnvAndConfigForDeploymentSelection(
ctx,
{
url: selectedDeployment.url,
deploymentName: selectedDeployment.deploymentFields.deploymentName,
teamSlug: selectedDeployment.deploymentFields.teamSlug,
projectSlug: selectedDeployment.deploymentFields.projectSlug,
deploymentType: selectedDeployment.deploymentFields.deploymentType
},
deploymentNameFromSelection(deploymentSelection)
);
} else {
await handleManuallySetUrlAndAdminKey(ctx, {
url: selectedDeployment.url,
adminKey: selectedDeployment.adminKey
});
}
return {
url: selectedDeployment.url,
adminKey: selectedDeployment.adminKey,
deploymentFields: selectedDeployment.deploymentFields
};
}
export async function _deploymentCredentialsOrConfigure(ctx, deploymentSelection, chosenConfiguration, cmdOptions, partitionId) {
const config = readGlobalConfig(ctx);
const globallyForceCloud = !!config?.optOutOfLocalDevDeploymentsUntilBetaOver;
if (globallyForceCloud && cmdOptions.local) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Can't specify --local when local deployments are disabled on this machine. Run `npx convex disable-local-deployments --undo-global` to allow use of --local."
});
}
switch (deploymentSelection.kind) {
case "existingDeployment":
await validateDeploymentSelectionForExistingDeployment(
ctx,
cmdOptions.selectionWithinProject,
deploymentSelection.deploymentToActOn.source
);
if (deploymentSelection.deploymentToActOn.deploymentFields === null) {
await handleManuallySetUrlAndAdminKey(ctx, {
url: deploymentSelection.deploymentToActOn.url,
adminKey: deploymentSelection.deploymentToActOn.adminKey
});
}
return {
url: deploymentSelection.deploymentToActOn.url,
adminKey: deploymentSelection.deploymentToActOn.adminKey,
deploymentFields: deploymentSelection.deploymentToActOn.deploymentFields
};
case "chooseProject": {
await ensureLoggedIn(ctx, {
overrideAuthUrl: cmdOptions.overrideAuthUrl,
overrideAuthClient: cmdOptions.overrideAuthClient,
overrideAuthUsername: cmdOptions.overrideAuthUsername,
overrideAuthPassword: cmdOptions.overrideAuthPassword
});
return await handleChooseProject(
ctx,
chosenConfiguration,
{
globallyForceCloud,
partitionId
},
cmdOptions
);
}
case "preview":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Use `npx convex deploy` to use preview deployments."
});
case "deploymentWithinProject": {
return await handleDeploymentWithinProject(ctx, {
chosenConfiguration,
targetProject: deploymentSelection.targetProject,
cmdOptions,
globallyForceCloud,
partitionId
});
}
case "anonymous": {
const hasAuth = ctx.bigBrainAuth() !== null;
if (hasAuth && deploymentSelection.deploymentName !== null) {
const shouldConfigure = chosenConfiguration !== null || await promptYesNo(ctx, {
message: `${CONVEX_DEPLOYMENT_ENV_VAR_NAME} is configured with deployment ${deploymentSelection.deploymentName}, which is not linked with your account. Would you like to choose a different project instead?`
});
if (!shouldConfigure) {
return await ctx.crash({
exitCode: 0,
errorType: "fatal",
printedMessage: `Run \`npx convex login --link-deployments\` first to link this deployment to your account, and then run \`npx convex dev\` again.`
});
}
return await handleChooseProject(
ctx,
chosenConfiguration,
{
globallyForceCloud,
partitionId
},
cmdOptions
);
}
const alreadyHasConfiguredAnonymousDeployment = deploymentSelection.deploymentName !== null && chosenConfiguration === null;
const shouldPromptForLogin = alreadyHasConfiguredAnonymousDeployment ? "no" : await promptOptions(ctx, {
message: "Welcome to Convex! Would you like to login to your account?",
choices: [
{
name: "Start without an account (run Convex locally)",
value: "no"
},
{ name: "Login or create an account", value: "yes" }
],
default: "no"
});
if (shouldPromptForLogin === "no") {
const result = await handleAnonymousDeployment(ctx, {
chosenConfiguration,
deploymentName: deploymentSelection.deploymentName,
...cmdOptions.localOptions
});
return {
adminKey: result.adminKey,
url: result.deploymentUrl,
deploymentFields: {
deploymentName: result.deploymentName,
deploymentType: "anonymous",
projectSlug: null,
teamSlug: null
}
};
}
return await handleChooseProject(
ctx,
chosenConfiguration,
{
globallyForceCloud,
partitionId
},
cmdOptions
);
}
}
}
async function handleDeploymentWithinProject(ctx, {
chosenConfiguration,
targetProject,
cmdOptions,
globallyForceCloud,
partitionId
}) {
const hasAuth = ctx.bigBrainAuth() !== null;
const loginMessage = hasAuth && shouldAllowAnonymousDevelopment() ? void 0 : `Tip: You can try out Convex without creating an account by clearing the ${CONVEX_DEPLOYMENT_ENV_VAR_NAME} environment variable.`;
await ensureLoggedIn(ctx, {
message: loginMessage,
overrideAuthUrl: cmdOptions.overrideAuthUrl,
overrideAuthClient: cmdOptions.overrideAuthClient,
overrideAuthUsername: cmdOptions.overrideAuthUsername,
overrideAuthPassword: cmdOptions.overrideAuthPassword
});
if (chosenConfiguration !== null) {
const result = await handleChooseProject(
ctx,
chosenConfiguration,
{
globallyForceCloud,
partitionId
},
cmdOptions
);
return result;
}
const accessResult = await checkAccessToSelectedProject(ctx, targetProject);
if (accessResult.kind === "noAccess") {
logMessage(ctx, "You don't have access to the selected project.");
const result = await handleChooseProject(
ctx,
chosenConfiguration,
{
globallyForceCloud,
partitionId
},
cmdOptions
);
return result;
}
const selectedDeployment = await loadSelectedDeploymentCredentials(
ctx,
{
kind: "deploymentWithinProject",
targetProject
},
cmdOptions.selectionWithinProject,
// We'll start running it below
{ ensureLocalRunning: false }
);
if (selectedDeployment.deploymentFields !== null && selectedDeployment.deploymentFields.deploymentType === "local") {
await handleLocalDeployment(ctx, {
teamSlug: selectedDeployment.deploymentFields.teamSlug,
projectSlug: selectedDeployment.deploymentFields.projectSlug,
forceUpgrade: cmdOptions.localOptions.forceUpgrade,
ports: cmdOptions.localOptions.ports,
backendVersion: cmdOptions.localOptions.backendVersion
});
}
return {
url: selectedDeployment.url,
adminKey: selectedDeployment.adminKey,
deploymentFields: selectedDeployment.deploymentFields
};
}
async function handleChooseProject(ctx, chosenConfiguration, args, cmdOptions) {
await ensureLoggedIn(ctx, {
overrideAuthUrl: cmdOptions.overrideAuthUrl,
overrideAuthClient: cmdOptions.overrideAuthClient,
overrideAuthUsername: cmdOptions.overrideAuthUsername,
overrideAuthPassword: cmdOptions.overrideAuthPassword
});
const project = await selectProject(ctx, chosenConfiguration, {
team: cmdOptions.team,
project: cmdOptions.project,
devDeployment: cmdOptions.devDeployment,
local: args.globallyForceCloud ? false : cmdOptions.local,
cloud: args.globallyForceCloud ? true : cmdOptions.cloud,
partitionId: args.partitionId
});
const deploymentOptions = cmdOptions.selectionWithinProject.kind === "prod" ? { kind: "prod" } : project.devDeployment === "local" ? { kind: "local", ...cmdOptions.localOptions } : { kind: "dev" };
const {
deploymentName,
deploymentUrl: url,
adminKey
} = await ensureDeploymentProvisioned(ctx, {
teamSlug: project.teamSlug,
projectSlug: project.projectSlug,
deploymentOptions,
partitionId: args.partitionId
});
return {
url,
adminKey,
deploymentFields: {
deploymentName,
deploymentType: deploymentOptions.kind,
projectSlug: project.projectSlug,
teamSlug: project.teamSlug
}
};
}
export async function handleManuallySetUrlAndAdminKey(ctx, cmdOptions) {
const { url, adminKey } = cmdOptions;
const didErase = await eraseDeploymentEnvVar(ctx);
if (didErase) {
logMessage(
ctx,
chalk.yellowBright(
`Removed the CONVEX_DEPLOYMENT environment variable from .env.local`
)
);
}
const envVarWrite = await writeConvexUrlToEnvFile(ctx, url);
if (envVarWrite !== null) {
logMessage(
ctx,
chalk.green(
`Saved the given --url as ${envVarWrite.envVar} to ${envVarWrite.envFile}`
)
);
}
return { url, adminKey };
}
export async function selectProject(ctx, chosenConfiguration, cmdOptions) {
const choice = chosenConfiguration !== "ask" && chosenConfiguration !== null ? chosenConfiguration : await askToConfigure(ctx);
switch (choice) {
case "new":
return selectNewProject(ctx, chosenConfiguration, cmdOptions);
case "existing":
return selectExistingProject(ctx, chosenConfiguration, cmdOptions);
default:
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "No project selected."
});
}
}
const cwd = path.basename(process.cwd());
async function selectNewProject(ctx, chosenConfiguration, config) {
const { teamSlug: selectedTeam, chosen: didChooseBetweenTeams } = await validateOrSelectTeam(ctx, config.team, "Team:");
let projectName = config.project || cwd;
let choseProjectInteractively = false;
if (!config.project) {
projectName = await promptString(ctx, {
message: "Project name:",
default: config.defaultProjectName || cwd
});
choseProjectInteractively = true;
}
const { devDeployment } = await selectDevDeploymentType(ctx, {
chosenConfiguration,
newOrExisting: "new",
teamSlug: selectedTeam,
userHasChosenSomethingInteractively: didChooseBetweenTeams || choseProjectInteractively,
projectSlug: void 0,
devDeploymentFromFlag: config.devDeployment,
forceDevDeployment: config.local ? "local" : config.cloud ? "cloud" : void 0
});
showSpinner(ctx, "Creating new Convex project...");
let projectSlug, teamSlug, projectsRemaining;
try {
({ projectSlug, teamSlug, projectsRemaining } = await createProject(ctx, {
teamSlug: selectedTeam,
projectName,
partitionId: config.partitionId,
// We have to create some deployment initially for a project.
deploymentTypeToProvision: devDeployment === "local" ? "prod" : "dev"
}));
} catch (err) {
logFailure(ctx, "Unable to create project.");
return await logAndHandleFetchError(ctx, err);
}
const teamMessage = didChooseBetweenTeams ? " in team " + chalk.bold(teamSlug) : "";
logFinishedStep(
ctx,
`Created project ${chalk.bold(
projectSlug
)}${teamMessage}, manage it at ${chalk.bold(
projectDashboardUrl(teamSlug, projectSlug)
)}`
);
if (projectsRemaining <= 2) {
logWarning(
ctx,
chalk.yellow.bold(
`Your account now has ${projectsRemaining} project${projectsRemaining === 1 ? "" : "s"} remaining.`
)
);
}
const { projectConfig: existingProjectConfig } = await readProjectConfig(ctx);
const configPath = await configFilepath(ctx);
const functionsPath = functionsDir(configPath, existingProjectConfig);
await doInitCodegen(ctx, functionsPath, true);
await doCodegen(ctx, functionsPath, "disable");
return { teamSlug, projectSlug, devDeployment };
}
async function selectExistingProject(ctx, chosenConfiguration, config) {
const { teamSlug, chosen } = await validateOrSelectTeam(
ctx,
config.team,
"Team:"
);
const projectSlug = await validateOrSelectProject(
ctx,
config.project,
teamSlug,
"Configure project",
"Project:"
);
if (projectSlug === null) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Run the command again to create a new project instead."
});
}
const { devDeployment } = await selectDevDeploymentType(ctx, {
chosenConfiguration,
newOrExisting: "existing",
teamSlug,
projectSlug,
userHasChosenSomethingInteractively: chosen || !config.project,
devDeploymentFromFlag: config.devDeployment,
forceDevDeployment: config.local ? "local" : config.cloud ? "cloud" : void 0
});
showSpinner(ctx, `Reinitializing project ${projectSlug}...
`);
const { projectConfig: existingProjectConfig } = await readProjectConfig(ctx);
const functionsPath = functionsDir(configName(), existingProjectConfig);
await doCodegen(ctx, functionsPath, "disable");
logFinishedStep(ctx, `Reinitialized project ${chalk.bold(projectSlug)}`);
return { teamSlug, projectSlug, devDeployment };
}
async function askToConfigure(ctx) {
if (!await hasProjects(ctx)) {
return "new";
}
return await promptOptions(ctx, {
message: "What would you like to configure?",
default: "new",
choices: [
{ name: "create a new project", value: "new" },
{ name: "choose an existing project", value: "existing" }
]
});
}
async function ensureDeploymentProvisioned(ctx, options) {
switch (options.deploymentOptions.kind) {
case "dev":
case "prod": {
const credentials = await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
ctx,
{
kind: "teamAndProjectSlugs",
teamSlug: options.teamSlug,
projectSlug: options.projectSlug
},
options.deploymentOptions.kind,
options.partitionId
);
return {
...credentials,
onActivity: null
};
}
case "local": {
const credentials = await handleLocalDeployment(ctx, {
teamSlug: options.teamSlug,
projectSlug: options.projectSlug,
...options.deploymentOptions
});
return credentials;
}
default:
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid deployment type: ${options.deploymentOptions.kind}`,
errForSentry: `Invalid deployment type: ${options.deploymentOptions.kind}`
});
}
}
export async function updateEnvAndConfigForDeploymentSelection(ctx, options, existingValue) {
const { configPath, projectConfig: existingProjectConfig } = await readProjectConfig(ctx);
const functionsPath = functionsDir(configName(), existingProjectConfig);
const { wroteToGitIgnore, changedDeploymentEnvVar } = await writeDeploymentEnvVar(
ctx,
options.deploymentType,
{
team: options.teamSlug,
project: options.projectSlug,
deploymentName: options.deploymentName
},
existingValue
);
const projectConfig = await upgradeOldAuthInfoToAuthConfig(
ctx,
existingProjectConfig,
functionsPath
);
await writeProjectConfig(ctx, projectConfig, {
deleteIfAllDefault: true
});
await finalizeConfiguration(ctx, {
deploymentType: options.deploymentType,
deploymentName: options.deploymentName,
url: options.url,
wroteToGitIgnore,
changedDeploymentEnvVar,
functionsPath: functionsDir(configPath, projectConfig)
});
}
//# sourceMappingURL=configure.js.map