UNPKG

convex

Version:

Client for the Convex Cloud

995 lines (916 loc) 31.5 kB
import { chalkStderr } from "chalk"; import path from "path"; import { z } from "zod"; import { Context } from "../../bundler/context.js"; import { TypescriptCompiler } from "./typecheck.js"; import { changeSpinner, logError, logFailure, logFinishedStep, logMessage, logWarning, showSpinner, } from "../../bundler/log.js"; import { Bundle, BundleHash, bundle, bundleAuthConfig, entryPointsByEnvironment, } from "../../bundler/index.js"; import { version } from "../version.js"; import { deploymentDashboardUrlPage } from "./dashboard.js"; import { functionsDir, ErrorData, loadPackageJson, deploymentFetch, deprecationCheckWarning, logAndHandleFetchError, ThrowingFetchError, currentPackageHomepage, } from "./utils/utils.js"; import { recursivelyDelete } from "./fsUtils.js"; import { NodeDependency } from "./deployApi/modules.js"; import { ComponentDefinitionPath } from "./components/definition/directoryStructure.js"; import { LocalDeploymentError, printLocalDeploymentOnError, } from "./localDeployment/errors.js"; import { debugIsolateBundlesSerially } from "../../bundler/debugBundle.js"; import { DeploymentType } from "./api.js"; export { productionProvisionHost, provisionHost } from "./utils/utils.js"; /** Type representing WorkOS AuthKit integration configuration. */ export interface AuthKitConfigureSettings { redirectUris?: string[]; appHomepageUrl?: string; corsOrigins?: string[]; } export interface AuthKitEnvironmentConfig { environmentType?: "development" | "staging" | "production"; configure?: false | AuthKitConfigureSettings; localEnvVars?: false | Record<string, string>; } export interface AuthKitConfig { dev?: AuthKitEnvironmentConfig; preview?: AuthKitEnvironmentConfig; prod?: AuthKitEnvironmentConfig; } /** * convex.json file parsing notes * * - Unknown fields at the top level and in node and codegen are preserved * so that older CLI versions can deploy new projects (this functionality * will be removed in the future). * - convex.json does not allow comments, but this could change in the future. * Previously it contained automatically set values like productionUrl * but it's more like a config file now. */ /** Type representing Convex project configuration. */ export interface ProjectConfig { // ⚠️ When updating this, please also update the file used by IDEs for autocompletion and validation: // -> npm-packages/convex/schemas/convex.schema.json functions: string; node: { externalPackages: string[]; // nodeVersion has no default value, its presence/absence is meaningful nodeVersion?: string | undefined; }; generateCommonJSApi: boolean; codegen: { staticApi: boolean; staticDataModel: boolean; legacyComponentApi?: boolean; fileType?: "ts" | "js/dts"; }; bundler?: { includeSourcesContent?: boolean; }; typescriptCompiler?: TypescriptCompiler; // WorkOS AuthKit integration configuration authKit?: AuthKitConfig | undefined; } export interface Config { projectConfig: ProjectConfig; modules: Bundle[]; nodeDependencies: NodeDependency[]; schemaId?: string; udfServerVersion?: string; nodeVersion?: string | undefined; } export interface ConfigWithModuleHashes { projectConfig: ProjectConfig; moduleHashes: BundleHash[]; nodeDependencies: NodeDependency[]; schemaId?: string; udfServerVersion?: string; } const DEFAULT_FUNCTIONS_PATH = "convex/"; /** Whether .ts file extensions should be used for generated code (default is false). */ export function usesTypeScriptCodegen(projectConfig: ProjectConfig): boolean { return projectConfig.codegen.fileType === "ts"; } /** Whether the new component API import style should be used (default is false) */ export function usesComponentApiImports(projectConfig: ProjectConfig): boolean { return projectConfig.codegen.legacyComponentApi === false; } /** * Get the authKit configuration from convex.json. */ export async function getAuthKitConfig( ctx: Context, projectConfig: ProjectConfig, ): Promise<AuthKitConfig | undefined> { // If there's an explicit authKit config, use it if ("authKit" in projectConfig) { return projectConfig.authKit; } // TODO remove this after a few versions // Migration help: is this one of the hardcoded templates that has special // behavior without a convex.json? Encourage them to upgrade the template. const homepage = await currentPackageHomepage(ctx); const isOldWorkOSTemplate = !!( homepage && [ "https://github.com/workos/template-convex-nextjs-authkit/#readme", "https://github.com/workos/template-convex-react-vite-authkit/#readme", "https://github.com:workos/template-convex-react-vite-authkit/#readme", "https://github.com/workos/template-convex-tanstack-start-authkit/#readme", ].includes(homepage) ); if (isOldWorkOSTemplate) { logWarning( "The template this project is based on has been updated to work with this version of Convex.", ); logWarning( "Please copy the convex.json from the latest template version or add an 'authKit' section.", ); logMessage("Learn more at https://docs.convex.dev/auth/authkit"); } } export async function getAuthKitEnvironmentConfig( ctx: Context, projectConfig: ProjectConfig, deploymentType: "dev" | "preview" | "prod", ): Promise<AuthKitEnvironmentConfig | undefined> { const authKitConfig = await getAuthKitConfig(ctx, projectConfig); return authKitConfig?.[deploymentType]; } /** Error parsing ProjectConfig representation. */ class ParseError extends Error {} // WorkOS AuthKit configuration schemas const AuthKitConfigureSchema = z.union([ z.literal(false), z.object({ redirectUris: z.array(z.string()).optional(), appHomepageUrl: z.string().optional(), corsOrigins: z.array(z.string()).optional(), }), ]); const AuthKitLocalEnvVarsSchema = z.union([ z.literal(false), z.record(z.string()), ]); const AuthKitEnvironmentConfigSchema = z.object({ environmentType: z.enum(["development", "staging", "production"]).optional(), configure: AuthKitConfigureSchema.optional(), localEnvVars: AuthKitLocalEnvVarsSchema.optional(), }); const AuthKitConfigSchema = z .object({ dev: AuthKitEnvironmentConfigSchema.optional(), preview: AuthKitEnvironmentConfigSchema.optional(), prod: AuthKitEnvironmentConfigSchema.optional(), }) .refine( (data) => { // Validation: environmentType only allowed in prod const devEnvType = data.dev?.environmentType; const previewEnvType = data.preview?.environmentType; if (devEnvType || previewEnvType) { return false; } return true; }, { message: "authKit.environmentType is only allowed in the prod section", path: ["environmentType"], }, ) .refine( (data) => { // Validation: localEnvVars only allowed for dev // Check preview doesn't have localEnvVars if ( data.preview?.localEnvVars !== undefined && data.preview?.localEnvVars !== false ) { return false; } // Check prod doesn't have localEnvVars if ( data.prod?.localEnvVars !== undefined && data.prod?.localEnvVars !== false ) { return false; } return true; }, { message: "authKit.localEnvVars is only supported for dev deployments. Preview and prod deployments must configure environment variables directly in the deployment platform.", path: ["localEnvVars"], }, ); // Separate Node and Codegen schemas so we can parse these loose or strict const NodeSchema = z.object({ externalPackages: z .array(z.string()) .default([]) .describe( "list of npm packages to install at deploy time instead of bundling. Packages with binaries should be added here.", ), nodeVersion: z .string() .optional() .describe("The Node.js version to use for Node.js functions"), }); const CodegenSchema = z.object({ staticApi: z .boolean() .default(false) .describe( "Use Convex function argument validators and return value validators to generate a typed API object", ), staticDataModel: z.boolean().default(false), // These optional fields have no defaults - their presence/absence is meaningful legacyComponentApi: z.boolean().optional(), fileType: z.enum(["ts", "js/dts"]).optional(), }); const BundlerSchema = z.object({ includeSourcesContent: z .boolean() .default(false) .describe( "Whether to include original source code in source maps. Set to false to reduce bundle size.", ), }); const refineToObject = <T extends z.ZodTypeAny>(schema: T) => schema.refine((val) => val !== null && !Array.isArray(val), { message: "Expected `convex.json` to contain an object", }); // Factory function to create schema with strict or passthrough behavior const createProjectConfigSchema = (strict: boolean) => { const nodeSchema = strict ? NodeSchema.strict() : NodeSchema.passthrough(); const codegenSchema = strict ? CodegenSchema.strict() : CodegenSchema.passthrough(); const bundlerSchema = strict ? BundlerSchema.strict() : BundlerSchema.passthrough(); const baseObject = z.object({ functions: z .string() .default(DEFAULT_FUNCTIONS_PATH) .describe("Relative file path to the convex directory"), node: nodeSchema.default({ externalPackages: [] }), codegen: codegenSchema.default({ staticApi: false, staticDataModel: false, }), bundler: bundlerSchema.default({ includeSourcesContent: false }).optional(), generateCommonJSApi: z.boolean().default(false), typescriptCompiler: z .enum(["tsc", "tsgo"]) .optional() .describe( "TypeScript compiler to use for typechecking (`@typescript/native-preview` must be installed to use `tsgo`)", ), // Optional $schema field for JSON schema validation in editors $schema: z.string().optional(), // WorkOS AuthKit integration configuration authKit: AuthKitConfigSchema.optional(), // Deprecated fields that have been deprecated for years, only here so we // know it's safe to delete them. project: z.string().optional(), team: z.string().optional(), prodUrl: z.string().optional(), }); // Apply strict or passthrough BEFORE refine const withStrictness = strict ? baseObject.strict() : baseObject.passthrough(); // Now apply the refinement return withStrictness.refine( (data) => { // Validate that generateCommonJSApi is not true when using TypeScript codegen if (data.generateCommonJSApi && data.codegen.fileType === "ts") { return false; } return true; }, { message: 'Cannot use `generateCommonJSApi: true` with `codegen.fileType: "ts"`. ' + "CommonJS modules require JavaScript generation. " + 'Either set `codegen.fileType: "js/dts"` or remove `generateCommonJSApi`.', path: ["generateCommonJSApi"], }, ); }; // Parse allowing extra fields (for forward compatibility) const ProjectConfigSchema = refineToObject(createProjectConfigSchema(false)); // Strict schema warn about extra keys const ProjectConfigSchemaStrict = refineToObject( createProjectConfigSchema(true), ); const warnedUnknownKeys = new Set<string>(); export function resetUnknownKeyWarnings() { warnedUnknownKeys.clear(); } /** Parse object to ProjectConfig. */ export async function parseProjectConfig( ctx: Context, obj: any, ): Promise<ProjectConfig> { if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: "Expected `convex.json` to contain an object", }); } try { // Try strict parse first to detect unknown keys return ProjectConfigSchemaStrict.parse(obj); } catch (error) { if (error instanceof z.ZodError) { // Check if all issues are unrecognized_keys issues const unknownKeyIssues = error.issues.filter( (issue) => issue.code === "unrecognized_keys", ); if ( unknownKeyIssues.length > 0 && unknownKeyIssues.length === error.issues.length ) { // All errors are just unknown keys - warn about them for (const issue of unknownKeyIssues) { if (issue.code === "unrecognized_keys") { const pathPrefix = issue.path.length > 0 ? issue.path.join(".") + "." : ""; const unknownKeys = issue.keys as string[]; const newUnknownKeys = unknownKeys.filter( (key) => !warnedUnknownKeys.has(pathPrefix + key), ); if (newUnknownKeys.length > 0) { const fullPath = issue.path.length > 0 ? `\`${issue.path.join(".")}\`` : "`convex.json`"; logMessage( chalkStderr.yellow( `Warning: Unknown ${newUnknownKeys.length === 1 ? "property" : "properties"} in ${fullPath}: ${newUnknownKeys.map((k) => `\`${k}\``).join(", ")}`, ), ); logMessage( chalkStderr.gray( " These properties will be preserved but are not recognized by this version of Convex.", ), ); // Track that we've warned about these keys newUnknownKeys.forEach((key) => warnedUnknownKeys.add(pathPrefix + key), ); } } } // Re-parse with passthrough schema to preserve unknown keys return ProjectConfigSchema.parse(obj); } // Handle validation errors we won't ignore if (error instanceof z.ZodError) { const issue = error.issues[0]; const pathStr = issue.path.join("."); const message = pathStr ? `\`${pathStr}\` in \`convex.json\`: ${issue.message}` : `\`convex.json\`: ${issue.message}`; return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: message, }); } } return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: (error as any).toString(), }); } } // Parse a deployment config returned by the backend, picking out // the fields we care about. function parseBackendConfig(obj: any): { functions: string; nodeVersion?: string; } { function throwParseError(message: string) { // Unexpected error // eslint-disable-next-line no-restricted-syntax throw new ParseError(message); } if (typeof obj !== "object") { throwParseError("Expected an object"); } const { functions, nodeVersion } = obj; if (typeof functions !== "string") { throwParseError("Expected functions to be a string"); } if (typeof nodeVersion !== "undefined" && typeof nodeVersion !== "string") { throwParseError("Expected nodeVersion to be a string"); } return { functions, ...((nodeVersion ?? null) !== null ? { nodeVersion: nodeVersion } : {}), }; } export function configName(): string { return "convex.json"; } export async function configFilepath(ctx: Context): Promise<string> { const configFn = configName(); // We used to allow src/convex.json, but no longer (as of 10/7/2022). // Leave an error message around to help people out. We can remove this // error message after a couple months. const preferredLocation = configFn; const wrongLocation = path.join("src", configFn); // Allow either location, but not both. const preferredLocationExists = ctx.fs.exists(preferredLocation); const wrongLocationExists = ctx.fs.exists(wrongLocation); if (preferredLocationExists && wrongLocationExists) { const message = `${chalkStderr.red(`Error: both ${preferredLocation} and ${wrongLocation} files exist!`)}\nConsolidate these and remove ${wrongLocation}.`; return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: message, }); } if (!preferredLocationExists && wrongLocationExists) { return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", printedMessage: `Error: Please move ${wrongLocation} to the root of your project`, }); } return preferredLocation; } export async function getFunctionsDirectoryPath(ctx: Context): Promise<string> { const { projectConfig, configPath } = await readProjectConfig(ctx); return functionsDir(configPath, projectConfig); } /** Read configuration from a local `convex.json` file. */ export async function readProjectConfig(ctx: Context): Promise<{ projectConfig: ProjectConfig; configPath: string; }> { if (!ctx.fs.exists("convex.json")) { // 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 = "react-scripts" in packages; return { projectConfig: { functions: isCreateReactApp ? `src/${DEFAULT_FUNCTIONS_PATH}` : DEFAULT_FUNCTIONS_PATH, node: { externalPackages: [], }, generateCommonJSApi: false, codegen: { staticApi: false, staticDataModel: false, }, }, configPath: configName(), }; } let projectConfig; const configPath = await configFilepath(ctx); try { projectConfig = await parseProjectConfig( ctx, JSON.parse(ctx.fs.readUtf8File(configPath)), ); } catch (err) { if (err instanceof ParseError || err instanceof SyntaxError) { logError(chalkStderr.red(`Error: Parsing "${configPath}" failed`)); logMessage(chalkStderr.gray(err.toString())); } else { logFailure( `Error: Unable to read project config file "${configPath}"\n` + " Are you running this command from the root directory of a Convex project? If so, run `npx convex dev` first.", ); if (err instanceof Error) { logError(chalkStderr.red(err.message)); } } return await ctx.crash({ exitCode: 1, errorType: "invalid filesystem data", errForSentry: err, // TODO -- move the logging above in here printedMessage: null, }); } return { projectConfig, configPath, }; } /** * Given a {@link ProjectConfig}, add in the bundled modules to produce the * complete config. */ export async function configFromProjectConfig( ctx: Context, projectConfig: ProjectConfig, configPath: string, verbose: boolean, ): Promise<{ config: Config; bundledModuleInfos: BundledModuleInfo[]; }> { const baseDir = functionsDir(configPath, projectConfig); // We bundle Node.js and Convex JS runtime functions entry points separately // since they execute on different platforms. const entryPoints = await entryPointsByEnvironment(ctx, baseDir); // es-build prints errors to console which would clobber our spinner. if (verbose) { showSpinner("Bundling modules for Convex's runtime..."); } const convexResult = await bundle({ ctx, dir: baseDir, entryPoints: entryPoints.isolate, generateSourceMaps: true, platform: "browser", }); if (verbose) { logMessage( "Convex's runtime modules: ", convexResult.modules.map((m) => m.path), ); } // Bundle node modules. if (verbose && entryPoints.node.length !== 0) { showSpinner("Bundling modules for Node.js runtime..."); } const nodeResult = await bundle({ ctx, dir: baseDir, entryPoints: entryPoints.node, generateSourceMaps: true, platform: "node", chunksFolder: path.join("_deps", "node"), externalPackagesAllowList: projectConfig.node.externalPackages, }); if (verbose && entryPoints.node.length !== 0) { logMessage( "Node.js runtime modules: ", nodeResult.modules.map((m) => m.path), ); if (projectConfig.node.externalPackages.length > 0) { logMessage( "Node.js runtime external dependencies (to be installed on the server): ", [...nodeResult.externalDependencies.entries()].map( (a) => `${a[0]}: ${a[1]}`, ), ); } } const modules = convexResult.modules; modules.push(...nodeResult.modules); modules.push(...(await bundleAuthConfig(ctx, baseDir))); const nodeDependencies: NodeDependency[] = []; for (const [moduleName, moduleVersion] of nodeResult.externalDependencies) { nodeDependencies.push({ name: moduleName, version: moduleVersion }); } const bundledModuleInfos: BundledModuleInfo[] = Array.from( convexResult.bundledModuleNames.keys(), ).map((moduleName) => { return { name: moduleName, platform: "convex", }; }); bundledModuleInfos.push( ...Array.from(nodeResult.bundledModuleNames.keys()).map( (moduleName): BundledModuleInfo => { return { name: moduleName, platform: "node", }; }, ), ); return { config: { projectConfig: projectConfig, modules: modules, nodeDependencies: nodeDependencies, // We're just using the version this CLI is running with for now. // This could be different than the version of `convex` the app runs with // if the CLI is installed globally. udfServerVersion: version, nodeVersion: projectConfig.node.nodeVersion, }, bundledModuleInfos, }; } /** * Bundle modules one by one for good bundler errors. */ export async function debugIsolateEndpointBundles( ctx: Context, projectConfig: ProjectConfig, configPath: string, ): Promise<void> { const baseDir = functionsDir(configPath, projectConfig); const entryPoints = await entryPointsByEnvironment(ctx, baseDir); if (entryPoints.isolate.length === 0) { logFinishedStep("No non-'use node' modules found."); } await debugIsolateBundlesSerially(ctx, { entryPoints: entryPoints.isolate, extraConditions: [], dir: baseDir, }); } /** * Read the config from `convex.json` and bundle all the modules. */ export async function readConfig( ctx: Context, verbose: boolean, ): Promise<{ config: Config; configPath: string; bundledModuleInfos: BundledModuleInfo[]; }> { const { projectConfig, configPath } = await readProjectConfig(ctx); const { config, bundledModuleInfos } = await configFromProjectConfig( ctx, projectConfig, configPath, verbose, ); return { config, configPath, bundledModuleInfos }; } /** * Ensure the functions directory exists. * * Note: This function no longer writes to or deletes `convex.json`. The config * file is now treated as user-owned and is not modified by the CLI. This allows * users to maintain their preferred formatting and any comments they may add * (if we later support JSONC parsing). */ export async function writeProjectConfig( ctx: Context, projectConfig: ProjectConfig, ) { const configPath = await configFilepath(ctx); ctx.fs.mkdir(functionsDir(configPath, projectConfig), { allowExisting: true, }); } export function removedExistingConfig( ctx: Context, configPath: string, options: { allowExistingConfig?: boolean }, ) { if (!options.allowExistingConfig) { return false; } recursivelyDelete(ctx, configPath); logFinishedStep(`Removed existing ${configPath}`); return true; } /** Pull configuration for the root app component from the given remote origin. */ export async function pullConfig( ctx: Context, project: string | undefined, team: string | undefined, origin: string, adminKey: string, ): Promise<ConfigWithModuleHashes> { const fetch = deploymentFetch(ctx, { deploymentUrl: origin, adminKey, }); changeSpinner("Downloading current deployment state..."); try { const res = await fetch("/api/get_config_hashes", { method: "POST", body: JSON.stringify({ version, adminKey }), }); deprecationCheckWarning(ctx, res); const data = await res.json(); const backendConfig = parseBackendConfig(data.config); const projectConfig = { ...backendConfig, node: { // This field is not stored in the backend, which is ok since it is also // not used to diff configs. externalPackages: [], nodeVersion: data.nodeVersion, }, // This field is not stored in the backend, it only affects the client. generateCommonJSApi: false, // This field is also not stored in the backend, it only affects the client. codegen: { staticApi: false, staticDataModel: false, }, project, team, prodUrl: origin, }; return { projectConfig, moduleHashes: data.moduleHashes, // TODO(presley): Add this to diffConfig(). nodeDependencies: data.nodeDependencies, udfServerVersion: data.udfServerVersion, }; } catch (err: unknown) { logFailure(`Error: Unable to pull deployment config from ${origin}`); return await logAndHandleFetchError(ctx, err); } } interface BundledModuleInfo { name: string; platform: "node" | "convex"; } /** * A component definition spec contains enough information to create bundles * of code that must be analyzed in order to construct a ComponentDefinition. * * Most paths are relative to the directory of the definitionPath. */ export type ComponentDefinitionSpec = { /** This path is relative to the app (root component) directory. */ definitionPath: ComponentDefinitionPath; /** Dependencies are paths to the directory of the dependency component definition from the app (root component) directory */ dependencies: ComponentDefinitionPath[]; // All other paths are relative to the directory of the definitionPath above. definition: Bundle; schema: Bundle; functions: Bundle[]; }; export type AppDefinitionSpec = Omit< ComponentDefinitionSpec, "definitionPath" > & { // Only app (root) component specs contain an auth bundle. auth: Bundle | null; }; export type ComponentDefinitionSpecWithoutImpls = Omit< ComponentDefinitionSpec, "schema" | "functions" >; export type AppDefinitionSpecWithoutImpls = Omit< AppDefinitionSpec, "schema" | "functions" | "auth" >; /** Generate a human-readable diff between the two configs. */ export function diffConfig( oldConfig: ConfigWithModuleHashes, newConfig: Config, ): { diffString: string } { let diff = ""; let versionMessage = ""; const matches = oldConfig.udfServerVersion === newConfig.udfServerVersion; if (oldConfig.udfServerVersion && (!newConfig.udfServerVersion || !matches)) { versionMessage += `[-] ${oldConfig.udfServerVersion}\n`; } if (newConfig.udfServerVersion && (!oldConfig.udfServerVersion || !matches)) { versionMessage += `[+] ${newConfig.udfServerVersion}\n`; } if (versionMessage) { diff += "Change the server's function version:\n"; diff += versionMessage; } if (oldConfig.projectConfig.node.nodeVersion !== newConfig.nodeVersion) { diff += "Change the server's version for Node.js actions:\n"; if (oldConfig.projectConfig.node.nodeVersion) { diff += `[-] ${oldConfig.projectConfig.node.nodeVersion}\n`; } if (newConfig.nodeVersion) { diff += `[+] ${newConfig.nodeVersion}\n`; } } return { diffString: diff }; } /** Handle an error from * legacy push path: * - /api/push_config * modern push paths: * - /api/deploy2/evaluate_push * - /api/deploy2/start_push * - /api/deploy2/finish_push * * finish_push errors are different from start_push errors and in theory could * be handled differently, but starting over works for all of them. */ export async function handlePushConfigError( ctx: Context, error: unknown, defaultMessage: string, deploymentName: string | null, deployment: | { deploymentUrl: string; adminKey: string; deploymentNotice: string; } | undefined, _deploymentType: DeploymentType | undefined, ): Promise<never> { const data: ErrorData | undefined = error instanceof ThrowingFetchError ? error.serverErrorData : undefined; if (data?.code === "AuthConfigMissingEnvironmentVariable") { const errorMessage = data.message || "(no error message given)"; const [, variableName] = errorMessage.match(/Environment variable (\S+)/i) ?? []; // DEPRECATED: This error path provisioning is being phased out in favor of // pre-flight provisioning that happens before the client bundle build. // We keep minimal logic here for backwards compatibility with older templates // that may still rely on this path. if (variableName === "WORKOS_CLIENT_ID" && deploymentName && deployment) { // For backwards compatibility with templates that haven't been updated, // we'll still show a helpful error message directing users to configure WorkOS. // But we no longer do automatic provisioning here since it happens too late // (after the client bundle has already been built with missing env vars). logWarning( "WORKOS_CLIENT_ID is not set; you can set it manually on the deployment or for hosted Convex deployments, use auto-provisioning.", ); logMessage( "Learn more at https://docs.convex.dev/auth/authkit/auto-provision", ); logMessage(""); } const envVarMessage = `Environment variable ${chalkStderr.bold( variableName, )} is used in auth config file but ` + `its value was not set.`; let setEnvVarInstructions = "Go set it in the dashboard or using `npx convex env set`"; // If `npx convex dev` is running using --url there might not be a configured deployment if (deploymentName !== null) { const variableQuery = variableName !== undefined ? `?var=${variableName}` : ""; const dashboardUrl = deploymentDashboardUrlPage( deploymentName, `/settings/environment-variables${variableQuery}`, ); setEnvVarInstructions = `Go to:\n\n ${chalkStderr.bold( dashboardUrl, )}\n\n to set it up. `; } await ctx.crash({ exitCode: 1, errorType: "invalid filesystem or env vars", errForSentry: error, printedMessage: envVarMessage + "\n" + setEnvVarInstructions, }); } if (data?.code === "RaceDetected") { // Environment variables or schema changed during push. This is a transient // error that should be retried immediately with exponential backoff. const message = data.message || "Schema or environment variables changed during push"; return await ctx.crash({ exitCode: 1, errorType: "transient", errForSentry: error, printedMessage: chalkStderr.yellow(message), }); } if (data?.code === "InternalServerError") { if (deploymentName?.startsWith("local-")) { printLocalDeploymentOnError(); return ctx.crash({ exitCode: 1, errorType: "fatal", errForSentry: new LocalDeploymentError( "InternalServerError while pushing to local deployment", ), printedMessage: defaultMessage, }); } } logFailure(defaultMessage); return await logAndHandleFetchError(ctx, error); }