UNPKG

convex

Version:

Client for the Convex Cloud

263 lines (234 loc) 6.74 kB
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; }