convex
Version:
Client for the Convex Cloud
263 lines (234 loc) • 6.74 kB
text/typescript
import chalk from "chalk";
import boxen from "boxen";
import {
pullConfig,
writeProjectConfig,
configFilepath,
readProjectConfig,
} from "./config.js";
import {
fatalServerErr,
functionsDir,
validateOrSelectTeam,
bigBrainAPI,
loadPackageJson,
} from "./utils.js";
import inquirer from "inquirer";
import ora from "ora";
import path from "path";
import { doCodegen, doInitCodegen } from "./codegen";
import { Context } from "./context.js";
import { dashboardUrl } from "../dashboard.js";
import { offerToWriteToEnv } from "./envvars.js";
const cwd = path.basename(process.cwd());
export async function init(
ctx: Context,
project: string | null,
team: string | null,
saveUrl: "yes" | "no" | "ask" = "ask"
) {
const configPath = await configFilepath(ctx);
if (ctx.fs.exists(configPath)) {
// Running init in a project with a convex.json file is a no-op.
console.error(chalk.green(`Found existing project config "${configPath}"`));
return;
}
// Do opt in to TOS and Privacy Policy stuff first.
const shouldContinue = await optins(ctx);
if (!shouldContinue) {
return await ctx.fatalError(1, undefined);
}
const selectedTeam = await validateOrSelectTeam(
ctx,
team,
"Choose which team to create this project in:"
);
let projectName: string = project || cwd;
if (process.stdin.isTTY && !project) {
projectName = (
await inquirer.prompt([
{
type: "input",
name: "project",
message: "Enter a name for your project:",
default: cwd,
},
])
).project;
}
const spinner = (ctx.spinner = ora({
text: "Creating new Convex project...\n",
stream: process.stdout,
}).start());
let projectSlug,
teamSlug,
prodUrl,
adminKey,
projectsRemaining,
projectConfig,
modules;
try {
({ projectSlug, teamSlug, prodUrl, adminKey, projectsRemaining } =
await create_project(ctx, selectedTeam, projectName));
({ projectConfig, modules } = await pullConfig(
ctx,
projectSlug,
teamSlug,
prodUrl,
adminKey
));
} catch (err) {
spinner.fail("Unable to create project.");
return await fatalServerErr(ctx, err);
}
spinner.succeed(`Successfully created project!`);
console.log(
chalk.green(`Your account now has ${projectsRemaining} projects remaining.`)
);
if (modules.length > 0) {
console.error(chalk.red("Error: Unexpected modules in new project"));
return await ctx.fatalError(1, undefined);
}
// create-react-app bans imports from outside of src, so we can just
// put the functions directory inside of src/ to work around this issue.
const packages = await loadPackageJson(ctx);
const isCreateReactApp = !!packages.filter(
({ name }) => name === "react-scripts"
).length;
if (isCreateReactApp) {
projectConfig.functions = `src/${projectConfig.functions}`;
}
await writeProjectConfig(ctx, projectConfig);
await doInitCodegen(
ctx,
functionsDir(configPath, projectConfig),
true // quiet
);
{
const { projectConfig, configPath } = await readProjectConfig(ctx);
await doCodegen({
ctx,
projectConfig,
configPath,
// Don't typecheck because there isn't any code to check yet.
typeCheckMode: "disable",
quiet: true,
});
}
const boxedText =
chalk.white("Project ") +
chalk.whiteBright.bold(projectName) +
chalk.white(" ready:\n") +
chalk.whiteBright.bold(`${teamSlug}/${projectSlug}`) +
"\n" +
chalk.whiteBright(projectConfig.prodUrl);
const boxenOptions = {
padding: 1,
margin: 1,
borderColor: "green",
backgroundColor: "#555555",
};
console.log(boxen(boxedText, boxenOptions));
console.log(`View this project at ${chalk.bold(await dashboardUrl(ctx))}`);
console.log("Configuration settings written to", chalk.bold(configPath));
console.log(
`Write Convex functions in ${chalk.bold(
functionsDir(configPath, projectConfig)
)}`
);
console.log();
console.log(
"Production deployment created at",
chalk.bold(projectConfig.prodUrl)
);
await offerToWriteToEnv(ctx, "prod", projectConfig.prodUrl, saveUrl);
console.log(chalk.bold("\nWe would love feedback at either:"));
console.log("- https://convex.dev/community");
console.log("- support@convex.dev");
console.log(
"\nSee documentation at",
chalk.bold("https://docs.convex.dev"),
"for next steps."
);
}
interface CreateProjectArgs {
projectName: string;
team: string;
backendVersionOverride?: string;
}
/** Provision a new empty project and return the origin. */
async function create_project(
ctx: Context,
team: string,
projectName: string
): Promise<{
projectSlug: string;
teamSlug: string;
prodUrl: string;
adminKey: string;
projectsRemaining: number;
}> {
const provisioningArgs: CreateProjectArgs = {
team,
backendVersionOverride: process.env.CONVEX_BACKEND_VERSION_OVERRIDE,
projectName,
};
const data = await bigBrainAPI(
ctx,
"POST",
"create_project",
provisioningArgs
);
const projectSlug = data.projectSlug;
const teamSlug = data.teamSlug;
const prodUrl = data.prodUrl;
const adminKey = data.adminKey;
const projectsRemaining = data.projectsRemaining;
if (
projectSlug === undefined ||
teamSlug === undefined ||
prodUrl === undefined ||
adminKey === undefined ||
projectsRemaining === undefined
) {
throw new Error(
"Unknown error during provisioning: " + JSON.stringify(data)
);
}
return { projectSlug, teamSlug, prodUrl, adminKey, projectsRemaining };
}
/// There are fields like version, but we keep them opaque
type OptIn = Record<string, unknown>;
type OptInToAccept = {
optIn: OptIn;
message: string;
};
type AcceptOptInsArgs = {
optInsAccepted: OptIn[];
};
// Returns whether we can proceed or not.
export async function optins(ctx: Context): Promise<boolean> {
const data = await bigBrainAPI(ctx, "POST", "check_opt_ins", {});
if (data.optInsToAccept.length === 0) {
return true;
}
for (const optInToAccept of data.optInsToAccept) {
const confirmed = (
await inquirer.prompt([
{
type: "confirm",
name: "confirmed",
message: optInToAccept.message,
},
])
).confirmed;
if (!confirmed) {
console.log("Please accept the Terms of Service to use Convex.");
return Promise.resolve(false);
}
}
const optInsAccepted = data.optInsToAccept.map((o: OptInToAccept) => o.optIn);
const args: AcceptOptInsArgs = { optInsAccepted };
await bigBrainAPI(ctx, "POST", "accept_opt_ins", args);
return true;
}