convex
Version:
Client for the Convex Cloud
713 lines (710 loc) • 24.9 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var deploymentCreate_exports = {};
__export(deploymentCreate_exports, {
createLocalDeployment: () => createLocalDeployment,
deploymentCreate: () => deploymentCreate,
fetchAvailableClasses: () => fetchAvailableClasses,
fetchAvailableRegions: () => fetchAvailableRegions,
resolveClassDetails: () => resolveClassDetails,
resolveRegionDetails: () => resolveRegionDetails
});
module.exports = __toCommonJS(deploymentCreate_exports);
var import_child_process = require("child_process");
var import_extra_typings = require("@commander-js/extra-typings");
var import_context = require("../bundler/context.js");
var import_log = require("../bundler/log.js");
var import_deploymentSelection = require("./lib/deploymentSelection.js");
var import_utils = require("./lib/utils/utils.js");
var import_deployment = require("./lib/deployment.js");
var import_deploymentSelect = require("./deploymentSelect.js");
var import_prompts = require("./lib/utils/prompts.js");
var import_chalk = require("chalk");
var import_deploymentSelector = require("./lib/deploymentSelector.js");
var import_expiration = require("./lib/expiration.js");
var import_download = require("./lib/localDeployment/download.js");
var import_filePaths = require("./lib/localDeployment/filePaths.js");
var import_utils2 = require("./lib/localDeployment/utils.js");
var import_bigBrain = require("./lib/localDeployment/bigBrain.js");
var import_localDeployment = require("./lib/localDeployment/localDeployment.js");
var import_run = require("./lib/localDeployment/run.js");
const SUPPORTED_TYPES = ["dev", "prod", "preview"];
const deploymentCreate = new import_extra_typings.Command("create").summary("Create a new deployment for a project").description(
"Create a new deployment for a project.\n\n Create a dev deployment and select it: `npx convex deployment create dev/my-new-feature --type dev --select`\n Create a prod deployment named \u201Cstaging\u201D: `npx convex deployment create staging --type prod`\n Create a local deployment: `npx convex deployment create local`\n"
).argument("[ref]").allowExcessArguments(false).addOption(
new import_extra_typings.Option("--type <type>", "Deployment type").choices(SUPPORTED_TYPES)
).option("--region <region>", "Deployment region").addOption(new import_extra_typings.Option("--class <class>", "Deployment class").hideHelp()).option(
"--select",
"Select the new deployment. This will update the Convex environment variables in .env.local. Subsequent `npx convex` commands will run against this deployment."
).option(
"--default",
"Make the new deployment your default production deployment (used by `npx convex deploy`) or your personal dev deployment."
).option(
"--expiration <value>",
'When the deployment expires (e.g. "none", "in 7 days", "2026-04-01T00:00:00Z", or a UNIX timestamp in seconds or milliseconds)'
).addOption(new import_extra_typings.Option("--expiry <value>").hideHelp()).addOption(new import_extra_typings.Option("--expires <value>").hideHelp()).action(async (refParam, options) => {
const expiration = options.expiration ?? options.expiry ?? options.expires;
const ctx = await (0, import_context.oneoffContext)({
url: void 0,
adminKey: void 0,
envFile: void 0
});
const currentDeployment = await (0, import_deploymentSelection.getDeploymentSelection)(ctx, {
url: void 0,
adminKey: void 0,
envFile: void 0
});
if (refParam !== void 0) {
if (refParam === "local") {
const cloudOnlyFlags = ["type", "region", "class", "default"];
for (const flag of cloudOnlyFlags) {
if (options[flag]) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `--${flag} cannot be used when creating a local deployment`
});
}
}
if (expiration !== void 0) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `--expiration cannot be used when creating a local deployment`
});
}
await createLocalDeployment(
ctx,
currentDeployment,
options.select ?? false
);
return;
}
}
const expiresAt = await resolveExpiresAtOrCrash(ctx, expiration);
const {
ref,
regionDetails,
classDetails,
projectId,
type,
isDefault,
teamSlug,
projectSlug
} = process.stdin.isTTY ? await resolveOptionsInteractively(
ctx,
currentDeployment,
refParam,
options
) : await resolveOptionsNoninteractively(
ctx,
currentDeployment,
refParam,
options
);
(0, import_log.showSpinner)(
`Creating ${type} deployment` + (regionDetails ? ` in region ${regionDetails.displayName}` : "") + (classDetails ? ` with class ${classDetails.type}` : "") + "..."
);
const created = (await (0, import_utils.typedPlatformClient)(ctx).POST(
"/projects/{project_id}/create_deployment",
{
params: {
path: { project_id: projectId }
},
body: {
type,
region: regionDetails?.name ?? null,
reference: ref ?? null,
isDefault,
...expiresAt !== void 0 ? { expiresAt } : {},
...classDetails ? { class: classDetails.type } : {}
}
}
)).data;
if (created.kind !== "cloud") {
const err = `Expected cloud deployment to be created but got ${created.kind}`;
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: err,
errForSentry: err
});
}
if (!options.select) {
(0, import_log.logFinishedStep)(
`Provisioned a ${created.isDefault ? "default " : ""}${created.deploymentType} deployment.`
);
if (type !== "prod") {
const selectRef = `${teamSlug}:${projectSlug}:${created.reference}`;
(0, import_log.logMessage)(
`
To make \`npx convex\` use this deployment, run ${import_chalk.chalkStderr.bold(`npx convex deployment select ${selectRef}`)}`
);
(0, import_log.logMessage)(
import_chalk.chalkStderr.gray(
"Hint: use `--select` to immediately select the newly created deployment."
)
);
}
}
if (options.select) {
const selection = {
kind: "deploymentWithinProject",
targetProject: {
kind: "teamAndProjectSlugs",
teamSlug,
projectSlug
},
selectionWithinProject: {
kind: "deploymentSelector",
selector: created.reference
}
};
await (0, import_deploymentSelect.saveSelectedDeployment)(
ctx,
created.reference,
selection,
(0, import_deploymentSelection.deploymentNameFromSelection)(currentDeployment)
);
}
});
async function createLocalDeployment(ctx, currentDeployment, select) {
const existing = (0, import_filePaths.loadProjectLocalConfig)(ctx);
if (existing) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "A local deployment already exists."
});
}
const { teamSlug, slug: projectSlug } = await resolveProject(
ctx,
currentDeployment
);
(0, import_log.showSpinner)("Downloading local backend...");
const { version } = await (0, import_download.ensureBackendBinaryDownloaded)(ctx, {
kind: "latest"
});
const { cloudPort, sitePort } = await (0, import_utils2.chooseLocalBackendPorts)(ctx);
(0, import_log.showSpinner)("Registering local deployment...");
const { deploymentName, adminKey } = await (0, import_bigBrain.bigBrainStart)(ctx, {
port: cloudPort,
projectSlug,
teamSlug,
instanceName: null
});
(0, import_filePaths.saveDeploymentConfig)(ctx, "local", deploymentName, {
backendVersion: version,
ports: { cloud: cloudPort, site: sitePort },
adminKey,
instanceSecret: import_utils2.LOCAL_BACKEND_INSTANCE_SECRET
});
(0, import_log.logFinishedStep)("Created local deployment.");
await (0, import_localDeployment.importDefaultEnvVars)(ctx, {
teamSlug,
projectSlug,
deploymentName,
deploymentUrl: (0, import_run.localDeploymentUrl)(cloudPort),
adminKey
});
if (select) {
const selection = {
kind: "deploymentWithinProject",
targetProject: {
kind: "deploymentName",
deploymentName,
deploymentType: "local"
},
selectionWithinProject: {
kind: "deploymentSelector",
selector: "local"
}
};
await (0, import_deploymentSelect.saveSelectedDeployment)(
ctx,
"local",
selection,
(0, import_deploymentSelection.deploymentNameFromSelection)(currentDeployment)
);
}
const devCommand = "npx convex dev";
if (select) {
(0, import_log.logMessage)(`
Run ${import_chalk.chalkStderr.bold(devCommand)} to start it.`);
} else {
(0, import_log.logMessage)(
`
To use this deployment, run:
` + import_chalk.chalkStderr.bold(` npx convex deployment select local
`) + ` Then, run ${import_chalk.chalkStderr.bold(devCommand)} to start it.`
);
}
}
async function resolveOptionsNoninteractively(ctx, currentDeployment, refParam, options) {
let ref;
let teamAndProject;
if (refParam) {
const result = parseSelectorForNewDeployment(refParam);
if (result.kind === "invalid") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: result.message
});
}
ref = result.ref;
teamAndProject = result.teamAndProject;
}
if (!ref && !options.default) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Specify a deployment ref or use --default:\n `npx convex deployment create my-deployment-ref --type dev`\n `npx convex deployment create --type prod --default`"
});
}
if (!options.type) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `--type is required (supported values: ${SUPPORTED_TYPES.join(", ")})`
});
}
const project = teamAndProject ? await (0, import_deploymentSelection.getProjectDetails)(ctx, {
kind: "teamAndProjectSlugs",
teamSlug: teamAndProject.teamSlug,
projectSlug: teamAndProject.projectSlug
}) : await resolveProject(ctx, currentDeployment);
const projectId = project.id;
let regionDetails = null;
if (options.region) {
const availableRegions = await fetchAvailableRegions(ctx, project.teamId);
regionDetails = await resolveRegionDetailsOrCrash(
ctx,
availableRegions,
options.region
);
}
let classDetails = null;
if (options.class) {
const availableClasses = await fetchAvailableClasses(ctx, project.teamId);
classDetails = await resolveClassDetailsOrCrash(
ctx,
availableClasses,
options.class
);
}
return {
ref,
isDefault: options.default ?? null,
projectId,
regionDetails,
classDetails,
type: options.type,
teamSlug: project.teamSlug,
projectSlug: project.slug
};
}
async function resolveOptionsInteractively(ctx, currentDeployment, refParam, options) {
let deploymentType;
if (options.type) {
deploymentType = logAndUse("type", options.type);
} else {
const dtypeChoices = [
{
name: "dev",
value: "dev"
},
{
name: "preview",
value: "preview"
},
{
name: "prod",
value: "prod"
}
];
deploymentType = await (0, import_prompts.promptOptions)(ctx, {
message: "Deployment type?",
choices: dtypeChoices
});
}
let ref;
let teamAndProject;
if (refParam) {
const result = parseSelectorForNewDeployment(refParam);
if (result.kind === "invalid") {
(0, import_log.logFailure)(result.message);
} else {
ref = logAndUse("ref", result.ref);
teamAndProject = result.teamAndProject;
}
}
while (ref === void 0) {
const gitDefault = defaultRef(localGitBranch(), deploymentType);
const input = await (0, import_prompts.promptString)(ctx, {
message: "What do you want to call this deployment?\n" + import_chalk.chalkStderr.reset.dim(
"The deployment reference will be used to identify your deployment on the dashboard and in CLI commands.\nExamples: staging, dev/james/feature"
) + "\n>",
...gitDefault !== void 0 ? { default: gitDefault } : {},
validate: validateTentativeReference
});
const result = parseSelectorForNewDeployment(input);
if (result.kind === "invalid") {
(0, import_log.logFailure)(result.message);
continue;
}
ref = result.ref;
teamAndProject = result.teamAndProject;
}
const project = teamAndProject ? await (0, import_deploymentSelection.getProjectDetails)(ctx, {
kind: "teamAndProjectSlugs",
teamSlug: teamAndProject.teamSlug,
projectSlug: teamAndProject.projectSlug
}) : await resolveProject(ctx, currentDeployment);
const availableRegions = await fetchAvailableRegions(ctx, project.teamId);
let regionDetails;
if (options.region) {
regionDetails = await resolveRegionDetailsOrCrash(
ctx,
availableRegions,
options.region
);
logAndUse("region", regionDetails.displayName);
} else {
const teams = (await (0, import_utils.typedBigBrainClient)(ctx).GET("/teams")).data;
const team = teams.find((team2) => team2.slug === project.teamSlug);
if (!team) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Error: Team ${project.teamSlug} not found.`
});
}
const regionName = team.defaultRegion ?? await (0, import_utils.selectRegion)(ctx, team.id, deploymentType);
regionDetails = await resolveRegionDetailsOrCrash(
ctx,
availableRegions,
regionName
);
if (team.defaultRegion) {
(0, import_log.logFinishedStep)(
`Using team default region of ${regionDetails.displayName}`
);
} else {
await (0, import_utils.logNoDefaultRegionMessage)(team.slug);
}
}
let classDetails = null;
if (options.class) {
const availableClasses = await fetchAvailableClasses(ctx, project.teamId);
classDetails = await resolveClassDetailsOrCrash(
ctx,
availableClasses,
options.class
);
logAndUse("class", classDetails.type);
}
return {
ref,
isDefault: options.default ?? null,
projectId: project.id,
regionDetails,
classDetails,
type: deploymentType,
teamSlug: project.teamSlug,
projectSlug: project.slug
};
}
function parseSelectorForNewDeployment(selectorString) {
const selector = (0, import_deploymentSelector.parseDeploymentSelector)(selectorString);
switch (selector.kind) {
case "local":
return {
kind: "invalid",
message: `"local" is reserved as an alias for your local deployment. To create one, run ${import_chalk.chalkStderr.bold("npx convex deployment create local")}`
};
case "deploymentName":
return {
kind: "invalid",
message: `"${selector.deploymentName}" is not a valid deployment reference. References can't look like "word-word-123" \u2014 that format is reserved for automatically-generated deployment names.`
};
case "inCurrentProject": {
const inner = selector.selector;
if (inner.kind === "dev") {
return {
kind: "invalid",
message: `"dev" is reserved as an alias for your default dev deployment.`
};
}
if (inner.kind === "prod") {
return {
kind: "invalid",
message: `"prod" is reserved as an alias for your default production deployment.`
};
}
return { kind: "valid", ref: inner.reference };
}
case "inProject": {
return {
kind: "invalid",
message: `Please use "team:project:ref" to specify the team when creating a new deployment in a different project.`
};
}
case "inTeamProject": {
const inner = selector.selector;
if (inner.kind === "dev") {
return {
kind: "invalid",
message: `"dev" is reserved as an alias for your default dev deployment.`
};
}
if (inner.kind === "prod") {
return {
kind: "invalid",
message: `"prod" is reserved as an alias for your default production deployment.`
};
}
return {
kind: "valid",
ref: inner.reference,
teamAndProject: {
teamSlug: selector.teamSlug,
projectSlug: selector.projectSlug
}
};
}
default:
selector;
return {
kind: "invalid",
message: "Unknown state. This is a bug in Convex."
};
}
}
async function resolveProject(ctx, deploymentSelection) {
switch (deploymentSelection.kind) {
case "existingDeployment": {
const { deploymentFields } = deploymentSelection.deploymentToActOn;
if (deploymentFields) {
return await (0, import_deploymentSelection.getProjectDetails)(ctx, {
kind: "deploymentName",
deploymentName: deploymentFields.deploymentName,
deploymentType: null
});
}
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Cannot infer project from the current deployment configuration. Use `team:project:ref` to specify team and project slugs."
});
}
case "deploymentWithinProject": {
return await (0, import_deploymentSelection.getProjectDetails)(ctx, deploymentSelection.targetProject);
}
case "preview": {
const slugs = await (0, import_deployment.getTeamAndProjectFromPreviewAdminKey)(
ctx,
deploymentSelection.previewDeployKey
);
return await (0, import_deploymentSelection.getProjectDetails)(ctx, {
kind: "teamAndProjectSlugs",
teamSlug: slugs.teamSlug,
projectSlug: slugs.projectSlug
});
}
case "chooseProject":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "No project configured yet. Use `team:project:ref` to specify team and project slugs."
});
case "anonymous":
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Cannot create a deployment in anonymous mode. Run `npx convex login` and configure a project first."
});
default: {
deploymentSelection;
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Unexpected deployment selection kind.`
});
}
}
}
const REGION_NAME_TO_ALIAS = {
"aws-us-east-1": "us",
"aws-eu-west-1": "eu"
};
const REGION_ALIAS_TO_NAME = Object.fromEntries(
Object.entries(REGION_NAME_TO_ALIAS).map(([name, alias]) => [alias, name])
);
async function fetchAvailableRegions(ctx, teamId) {
const regionsResponse = (await (0, import_utils.typedPlatformClient)(ctx).GET(
"/teams/{team_id}/list_deployment_regions",
{
params: {
path: { team_id: `${teamId}` }
}
}
)).data;
return regionsResponse.items.filter((item) => item.available);
}
function resolveRegionDetails(availableRegions, region) {
const resolvedRegion = REGION_ALIAS_TO_NAME[region] ?? region;
return availableRegions.find((item) => item.name === resolvedRegion) ?? null;
}
async function resolveRegionDetailsOrCrash(ctx, availableRegions, region) {
const regionDetails = resolveRegionDetails(availableRegions, region);
if (!regionDetails) {
return await crashInvalidRegion(ctx, availableRegions, region);
}
return regionDetails;
}
function invalidRegionMessage(availableRegions, region) {
const formatted = availableRegions.map(
(item) => ` Use \`--region ${REGION_NAME_TO_ALIAS[item.name] ?? item.name}\` for ${item.displayName}`
).join("\n");
return `Invalid region "${region}".
` + formatted;
}
async function crashInvalidRegion(ctx, availableRegions, region) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: invalidRegionMessage(availableRegions, region)
});
}
async function fetchAvailableClasses(ctx, teamId) {
const classesResponse = (await (0, import_utils.typedPlatformClient)(ctx).GET(
"/teams/{team_id}/list_deployment_classes",
{
params: {
path: { team_id: `${teamId}` }
}
}
)).data;
return classesResponse.items.filter((item) => item.available);
}
function resolveClassDetails(availableClasses, className) {
return availableClasses.find((item) => item.type === className) ?? null;
}
async function resolveClassDetailsOrCrash(ctx, availableClasses, className) {
const classDetails = resolveClassDetails(availableClasses, className);
if (!classDetails) {
return await crashInvalidClass(ctx, availableClasses, className);
}
return classDetails;
}
function invalidClassMessage(availableClasses, className) {
const formatted = availableClasses.map((item) => ` \`--class ${item.type}\``).join("\n");
return `Invalid class "${className}".
Available classes:
` + formatted;
}
async function crashInvalidClass(ctx, availableClasses, className) {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: invalidClassMessage(availableClasses, className)
});
}
async function resolveExpiresAtOrCrash(ctx, expiration) {
if (!expiration) {
return void 0;
}
const parsed = (0, import_expiration.parseExpiration)(expiration);
if (parsed.kind === "error") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: parsed.message
});
}
const now = Date.now();
const resolved = (0, import_expiration.resolveExpiration)(parsed, now);
if (resolved !== null) {
const validation = (0, import_expiration.validateExpiration)(resolved, now);
if (validation.kind === "error") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: validation.message
});
}
}
return resolved;
}
function logAndUse(label, value) {
(0, import_log.logFinishedStep)(`Using ${label}: ${import_chalk.chalkStderr.bold(value)}`);
return value;
}
function validateTentativeReference(tentativeReference) {
if (tentativeReference.length < 3) {
return "References must be at least 3 characters";
}
if (tentativeReference.length > 100) {
return "References must be at most 100 characters";
}
if (!/^[a-z0-9/-]+$/.test(tentativeReference)) {
return "References can only contain lowercase letters, numbers, `-`, and `/`";
}
if (tentativeReference === "dev") {
return '"dev" is reserved as an alias for your default dev deployment.';
}
if (tentativeReference === "prod") {
return '"prod" is reserved as an alias for your default production deployment.';
}
if (tentativeReference === "local") {
return `"local" is reserved as an alias for your local deployment. To create one, run ${import_chalk.chalkStderr.bold("npx convex deployment create local")}`;
}
if (/^[a-z]+-[a-z]+-\d+$/.test(tentativeReference)) {
return `References can't look like "word-word-123" \u2014 that format is reserved for automatically-generated deployment names. Try something like dev/my-feature or staging instead.`;
}
return true;
}
function localGitBranch() {
try {
const branch = (0, import_child_process.execSync)("git rev-parse --abbrev-ref HEAD", {
stdio: ["pipe", "pipe", "pipe"],
timeout: 5e3
}).toString().trim();
if (!branch || branch === "HEAD" || branch === "main" || branch === "master") {
return null;
}
return branch;
} catch {
return null;
}
}
function defaultRef(branch, deploymentType) {
if (deploymentType !== "dev" && deploymentType !== "preview") {
return void 0;
}
if (!branch) return void 0;
const slug = branch.replace(/[^a-z0-9/-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
if (!slug) return void 0;
const ref = `${deploymentType}/${slug}`;
const valid = validateTentativeReference(ref);
if (valid !== true) return void 0;
return ref;
}
//# sourceMappingURL=deploymentCreate.js.map