convex
Version:
Client for the Convex Cloud
202 lines (189 loc) • 5.76 kB
text/typescript
import chalk from "chalk";
import {
Context,
changeSpinner,
logFailure,
logMessage,
} from "../../bundler/context.js";
import { doCodegen } from "./codegen.js";
import {
Config,
configFromProjectConfig,
diffConfig,
pullConfig,
pushConfig,
readProjectConfig,
} from "./config.js";
import { pushSchema } from "./indexes.js";
import { typeCheckFunctionsInMode } from "./typecheck.js";
import { ensureHasConvexDependency, functionsDir } from "./utils.js";
import path from "path";
export type PushOptions = {
adminKey: string;
verbose: boolean;
dryRun: boolean;
typecheck: "enable" | "try" | "disable";
debug: boolean;
debugBundlePath?: string;
codegen: boolean;
url: string;
};
export async function runPush(ctx: Context, options: PushOptions) {
const timeRunPushStarts = performance.now();
const { configPath, projectConfig } = await readProjectConfig(ctx);
const origin = options.url;
const verbose = options.verbose || options.dryRun;
await ensureHasConvexDependency(ctx, "push");
if (!options.codegen) {
logMessage(
ctx,
chalk.gray("Skipping codegen. Remove --codegen=disable to enable."),
);
// Codegen includes typechecking, so if we're skipping it, run the type
// check manually on the query and mutation functions
const funcDir = functionsDir(configPath, projectConfig);
await typeCheckFunctionsInMode(ctx, options.typecheck, funcDir);
} else {
await doCodegen({
ctx,
functionsDirectoryPath: functionsDir(configPath, projectConfig),
typeCheckMode: options.typecheck,
dryRun: options.dryRun,
debug: options.debug,
quiet: true,
});
if (verbose) {
logMessage(ctx, chalk.green("Codegen finished."));
}
}
const timeBundleStarts = performance.now();
const { config: localConfig, bundledModuleInfos } =
await configFromProjectConfig(ctx, projectConfig, configPath, verbose);
if (options.debugBundlePath) {
await handleDebugBundlePath(ctx, options.debugBundlePath, localConfig);
logMessage(
ctx,
`Wrote bundle and metadata to ${options.debugBundlePath}. Skipping rest of push.`,
);
return;
}
const timeSchemaPushStarts = performance.now();
const { schemaId, schemaState } = await pushSchema(
ctx,
origin,
options.adminKey,
functionsDir(configPath, localConfig.projectConfig),
options.dryRun,
);
const timeConfigPullStarts = performance.now();
const remoteConfigWithModuleHashes = await pullConfig(
ctx,
undefined,
undefined,
origin,
options.adminKey,
);
changeSpinner(ctx, "Diffing local code and deployment state");
const { diffString, stats } = diffConfig(
remoteConfigWithModuleHashes,
localConfig,
);
if (diffString === "" && schemaState?.state === "active") {
if (verbose) {
const msg =
localConfig.modules.length === 0
? `No functions found in ${localConfig.projectConfig.functions}`
: "Config already synced";
logMessage(
ctx,
chalk.gray(
`${
options.dryRun
? "Command would skip function push"
: "Function push skipped"
}: ${msg}.`,
),
);
}
return;
}
if (verbose) {
logMessage(
ctx,
chalk.bold(
`Remote config ${
options.dryRun ? "would" : "will"
} be overwritten with the following changes:`,
),
);
logMessage(ctx, diffString);
}
if (options.dryRun) {
return;
}
// Note that this is not quite a user pain metric: we're missing any time
// spent making and retrying this network request and receiving the response.
const timePushStarts = performance.now();
const timing = {
typecheck: (timeBundleStarts - timeRunPushStarts) / 1000,
bundle: (timeSchemaPushStarts - timeBundleStarts) / 1000,
schemaPush: (timeConfigPullStarts - timeSchemaPushStarts) / 1000,
codePull: (timePushStarts - timeConfigPullStarts) / 1000,
totalBeforePush: (timePushStarts - timeRunPushStarts) / 1000,
moduleDiffStats: stats,
};
await pushConfig(
ctx,
localConfig,
options.adminKey,
options.url,
timing,
schemaId,
bundledModuleInfos,
);
}
async function handleDebugBundlePath(
ctx: Context,
debugBundleDir: string,
config: Config,
) {
if (!ctx.fs.exists(debugBundleDir)) {
ctx.fs.mkdir(debugBundleDir);
} else if (!ctx.fs.stat(debugBundleDir).isDirectory()) {
logFailure(
ctx,
`Path \`${debugBundleDir}\` is not a directory. Please choose an empty directory for \`--debug-bundle-path\`.`,
);
await ctx.crash(1, "fatal");
} else if (ctx.fs.listDir(debugBundleDir).length !== 0) {
logFailure(
ctx,
`Directory \`${debugBundleDir}\` is not empty. Please remove it or choose an empty directory for \`--debug-bundle-path\`.`,
);
await ctx.crash(1, "fatal");
}
ctx.fs.writeUtf8File(
path.join(debugBundleDir, "fullConfig.json"),
JSON.stringify(config),
);
for (const moduleInfo of config.modules) {
const trimmedPath = moduleInfo.path.endsWith(".js")
? moduleInfo.path.slice(0, moduleInfo.path.length - ".js".length)
: moduleInfo.path;
const environmentDir = path.join(debugBundleDir, moduleInfo.environment);
ctx.fs.mkdir(path.dirname(path.join(environmentDir, `${trimmedPath}.js`)), {
allowExisting: true,
recursive: true,
});
ctx.fs.writeUtf8File(
path.join(environmentDir, `${trimmedPath}.js`),
moduleInfo.source,
);
if (moduleInfo.sourceMap !== undefined) {
ctx.fs.writeUtf8File(
path.join(environmentDir, `${trimmedPath}.js.map`),
moduleInfo.sourceMap,
);
}
}
}