UNPKG

convex

Version:

Client for the Convex Cloud

207 lines (206 loc) 7.17 kB
"use strict"; import boxen from "boxen"; import chalk from "chalk"; import { Command, Option } from "commander"; import path from "path"; import { performance } from "perf_hooks"; import { getDevDeployment, getUrlAndAdminKey } from "./lib/api"; import { readProjectConfig } from "./lib/config"; import { oneoffContext } from "./lib/context"; import { offerToWriteToEnv } from "./lib/envvars"; import { checkAuthorization, performLogin } from "./lib/login"; import { runPush } from "./lib/push"; import { ensureProjectDirectory, formatDuration } from "./lib/utils"; import { FatalError, WatchContext, Watcher } from "./lib/watch"; export const dev = new Command("dev").description( "Watch the local filesystem. When your Convex functions change, push them to your dev deployment and update generated code." ).option("-v, --verbose", "Show full listing of changes").addOption( new Option( "--typecheck <mode>", `Check TypeScript files with \`tsc --noEmit\`.` ).choices(["enable", "try", "disable"]).default("try") ).option("--save-url", "Save the dev deployment URL to .env.local").option("--no-save-url", "Do not save dev deployment URL to .env.local").addOption( new Option( "--prod", "Develop live against this project's production deployment." ).hideHelp() ).addOption(new Option("--trace-events").hideHelp()).addOption(new Option("--once").hideHelp()).addOption(new Option("--admin-key <adminKey>").hideHelp()).addOption(new Option("--url <url>").hideHelp()).addOption( new Option("--codegen <mode>", "Regenerate code in `convex/_generated/`").choices(["enable", "disable"]).default("enable") ).action(async (cmdOptions) => { const ctx = oneoffContext; const saveUrl = cmdOptions.saveUrl === true ? "yes" : cmdOptions.saveUrl === false ? "no" : "ask"; if (!cmdOptions.url || !cmdOptions.adminKey) { if (!await checkAuthorization(ctx)) { await performLogin(ctx); } } await ensureProjectDirectory(ctx, true); const config = await readProjectConfig(oneoffContext); const projectSlug = config.projectConfig.project; const teamSlug = config.projectConfig.team; let deployment; if (!cmdOptions.url || !cmdOptions.adminKey) { if (cmdOptions.prod) { deployment = await getUrlAndAdminKey( ctx, config.projectConfig.project, config.projectConfig.team, "prod" ); console.error("Found deployment ready"); } else { deployment = await getDevDeployment(oneoffContext, { projectSlug, teamSlug }); } } const adminKey = cmdOptions.adminKey ?? deployment?.adminKey; const url = cmdOptions.url ?? deployment?.url; const options = { adminKey, verbose: !!cmdOptions.verbose, dryRun: false, typecheck: cmdOptions.typecheck, debug: false, codegen: cmdOptions.codegen === "enable", url }; let watcher; let numFailures = 0; await offerToWriteToEnv(ctx, "dev", url, saveUrl); const boxedText = chalk.whiteBright.bold( `${cmdOptions.prod ? "Production" : "Development"} deployment at ${url} ready!` ) + chalk.white( "\n\nKeep this command running to sync Convex functions when they change." ); const boxenOptions = { align: "center", padding: 1, margin: 1, borderColor: "green", backgroundColor: "#555555" }; if (!cmdOptions.once) { console.log(boxen(boxedText, boxenOptions)); } while (true) { console.log("Preparing Convex functions..."); const start = performance.now(); const ctx2 = new WatchContext(cmdOptions.traceEvents); const config2 = await readProjectConfig(ctx2); if (projectSlug !== config2.projectConfig.project || teamSlug !== config2.projectConfig.team) { console.log("Detected a change in your `convex.json`. Exiting..."); return await ctx2.fatalError(1, "fs"); } try { await runPush(ctx2, options); const end = performance.now(); numFailures = 0; console.log( chalk.green( `Convex functions ready! (${formatDuration(end - start)})` ) ); } catch (e) { if (!(e instanceof FatalError) || !e.reason) { throw e; } if (e.reason === "network") { const delay = nextBackoff(numFailures); numFailures += 1; console.log( chalk.yellow( `Failed due to network error, retrying in ${formatDuration( delay )}...` ) ); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } console.assert(e.reason === "fs"); if (cmdOptions.once) { await ctx2.fatalError(1, "fs"); } } if (cmdOptions.once) { return; } const observations = ctx2.fs.finalize(); if (observations === "invalidated") { console.log("Filesystem changed during push, retrying..."); continue; } if (!watcher) { watcher = new Watcher(observations); await watcher.ready(); } watcher.update(observations); let anyChanges = false; do { await watcher.waitForEvent(); for (const event of watcher.drainEvents()) { if (cmdOptions.traceEvents) { console.log( "Processing", event.name, path.relative("", event.absPath) ); } const result = observations.overlaps(event); if (result.overlaps) { const relPath = path.relative("", event.absPath); if (cmdOptions.traceEvents) { console.log(`${relPath} ${result.reason}, rebuilding...`); } anyChanges = true; break; } } } while (!anyChanges); let deadline = performance.now() + quiescenceDelay; while (true) { const now = performance.now(); if (now >= deadline) { break; } const remaining = deadline - now; if (cmdOptions.traceEvents) { console.log(`Waiting for ${formatDuration(remaining)} to quiesce...`); } const remainingWait = new Promise( (resolve) => setTimeout(() => resolve("timeout"), deadline - now) ); const result = await Promise.race([ remainingWait, watcher.waitForEvent().then(() => "newEvents") ]); if (result === "newEvents") { for (const event of watcher.drainEvents()) { const result2 = observations.overlaps(event); if (result2.overlaps) { if (cmdOptions.traceEvents) { console.log( `Received an overlapping event at ${event.absPath}, delaying push.` ); } deadline = performance.now() + quiescenceDelay; } } } else { console.assert(result === "timeout"); } } } }); const initialBackoff = 500; const maxBackoff = 16e3; const quiescenceDelay = 500; function nextBackoff(prevFailures) { const baseBackoff = initialBackoff * Math.pow(2, prevFailures); const actualBackoff = Math.min(baseBackoff, maxBackoff); const jitter = actualBackoff * (Math.random() - 0.5); return actualBackoff + jitter; } //# sourceMappingURL=dev.js.map