Skip to content

Commit c4d2cd5

Browse files
sshaderConvex, Inc.
authored and
Convex, Inc.
committed
Change ctx.crash to handle printing a message (#28740)
GitOrigin-RevId: ecd2567dd6105abe76db7860ce62aa82b03a6839
1 parent 8fd1ba7 commit c4d2cd5

36 files changed

+876
-561
lines changed

npm-packages/convex/src/bundler/context.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,15 @@ export interface Context {
3535
deprecationMessagePrinted: boolean;
3636
spinner: Ora | undefined;
3737
// Reports to Sentry and either throws FatalError or exits the process.
38-
// Does not print the error.
39-
crash(exitCode: number, errorType?: ErrorType, err?: any): Promise<never>;
38+
// Prints the `printedMessage` if provided
39+
crash(args: {
40+
exitCode: number;
41+
errorType: ErrorType;
42+
errForSentry?: any;
43+
printedMessage: string | null;
44+
}): Promise<never>;
4045
}
4146

42-
export const oneoffContext: Context = {
43-
fs: nodeFs,
44-
deprecationMessagePrinted: false,
45-
spinner: undefined,
46-
async crash(exitCode: number, _errorType?: ErrorType, err?: any) {
47-
logVerbose(
48-
oneoffContext,
49-
`Crashing with exit code ${exitCode}, error: ${_errorType?.toString()} ${err?.toString()}`,
50-
);
51-
return await flushAndExit(exitCode, err);
52-
},
53-
};
54-
5547
async function flushAndExit(exitCode: number, err?: any) {
5648
if (err) {
5749
Sentry.captureException(err);
@@ -61,6 +53,33 @@ async function flushAndExit(exitCode: number, err?: any) {
6153
return process.exit(exitCode);
6254
}
6355

56+
export type OneoffCtx = Context & {
57+
// Generally `ctx.crash` is better to use since it handles printing a message
58+
// for the user, and then calls this.
59+
//
60+
// This function reports to Sentry + exits the process, but does not handle
61+
// printing a message for the user.
62+
flushAndExit: (exitCode: number, err?: any) => Promise<never>;
63+
};
64+
65+
export const oneoffContext: OneoffCtx = {
66+
fs: nodeFs,
67+
deprecationMessagePrinted: false,
68+
spinner: undefined,
69+
async crash(args: {
70+
exitCode: number;
71+
errorType?: ErrorType;
72+
errForSentry?: any;
73+
printedMessage: string | null;
74+
}) {
75+
if (args.printedMessage !== null) {
76+
logFailure(oneoffContext, args.printedMessage);
77+
}
78+
return await flushAndExit(args.exitCode, args.errForSentry);
79+
},
80+
flushAndExit,
81+
};
82+
6483
// console.error before it started being red by default in Node v20
6584
function logToStderr(...args: unknown[]) {
6685
process.stderr.write(`${format(...args)}\n`);

npm-packages/convex/src/bundler/external.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PluginBuild } from "esbuild";
22
import type { Plugin } from "esbuild";
3-
import { Context, logFailure } from "./context.js";
3+
import { Context } from "./context.js";
44
import path from "path";
55

66
import { findUp } from "find-up";
@@ -119,11 +119,11 @@ export async function computeExternalPackages(
119119
const packageJsonString = ctx.fs.readUtf8File(packageJsonPath);
120120
packageJson = JSON.parse(packageJsonString);
121121
} catch (error: any) {
122-
logFailure(
123-
ctx,
124-
`Couldn't parse "${packageJsonPath}". Make sure it's a valid JSON. Error: ${error}`,
125-
);
126-
return await ctx.crash(1, "invalid filesystem data");
122+
return await ctx.crash({
123+
exitCode: 1,
124+
errorType: "invalid filesystem data",
125+
printedMessage: `Couldn't parse "${packageJsonPath}". Make sure it's a valid JSON. Error: ${error}`,
126+
});
127127
}
128128

129129
for (const key of [
@@ -141,11 +141,11 @@ export async function computeExternalPackages(
141141
}
142142

143143
if (typeof packageJsonVersion !== "string") {
144-
logFailure(
145-
ctx,
146-
`Invalid "${packageJsonPath}". "${key}.${packageName}" version has type ${typeof packageJsonVersion}.`,
147-
);
148-
return await ctx.crash(1, "invalid filesystem data");
144+
return await ctx.crash({
145+
exitCode: 1,
146+
errorType: "invalid filesystem data",
147+
printedMessage: `Invalid "${packageJsonPath}". "${key}.${packageName}" version has type ${typeof packageJsonVersion}.`,
148+
});
149149
}
150150

151151
if (
@@ -226,20 +226,20 @@ export async function findExactVersionAndDependencies(
226226
const packageJsonString = ctx.fs.readUtf8File(modulePackageJsonPath);
227227
modulePackageJson = JSON.parse(packageJsonString);
228228
} catch (error: any) {
229-
logFailure(
230-
ctx,
231-
`Missing "${modulePackageJsonPath}", which is required for
229+
return await ctx.crash({
230+
exitCode: 1,
231+
errorType: "invalid filesystem data",
232+
printedMessage: `Missing "${modulePackageJsonPath}", which is required for
232233
installing external package "${moduleName}" configured in convex.json.`,
233-
);
234-
return await ctx.crash(1, "invalid filesystem data");
234+
});
235235
}
236236
if (modulePackageJson["version"] === undefined) {
237-
logFailure(
238-
ctx,
239-
`"${modulePackageJsonPath}" misses a 'version' field. which is required for
237+
return await ctx.crash({
238+
exitCode: 1,
239+
errorType: "invalid filesystem data",
240+
printedMessage: `"${modulePackageJsonPath}" misses a 'version' field. which is required for
240241
installing external package "${moduleName}" configured in convex.json.`,
241-
);
242-
return await ctx.crash(1, "invalid filesystem data");
242+
});
243243
}
244244

245245
const peerAndOptionalDependencies = new Set<string>();
@@ -248,11 +248,11 @@ export async function findExactVersionAndDependencies(
248248
modulePackageJson[key] ?? {},
249249
)) {
250250
if (typeof packageJsonVersion !== "string") {
251-
logFailure(
252-
ctx,
253-
`Invalid "${modulePackageJsonPath}". "${key}.${packageName}" version has type ${typeof packageJsonVersion}.`,
254-
);
255-
return await ctx.crash(1, "invalid filesystem data");
251+
return await ctx.crash({
252+
exitCode: 1,
253+
errorType: "invalid filesystem data",
254+
printedMessage: `Invalid "${modulePackageJsonPath}". "${key}.${packageName}" version has type ${typeof packageJsonVersion}.`,
255+
});
256256
}
257257
peerAndOptionalDependencies.add(packageName);
258258
}

npm-packages/convex/src/bundler/index.ts

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { parse as parseAST } from "@babel/parser";
55
import { Identifier, ImportSpecifier } from "@babel/types";
66
import * as Sentry from "@sentry/node";
77
import { Filesystem } from "./fs.js";
8-
import { Context, logFailure, logWarning } from "./context.js";
8+
import { Context, logWarning } from "./context.js";
99
import { wasmPlugin } from "./wasm.js";
1010
import {
1111
ExternalPackage,
@@ -113,7 +113,11 @@ async function doEsbuild(
113113
);
114114
// Consider this a transient error so we'll try again and hopefully
115115
// no files change right after esbuild next time.
116-
return await ctx.crash(1, "transient");
116+
return await ctx.crash({
117+
exitCode: 1,
118+
errorType: "transient",
119+
printedMessage: null,
120+
});
117121
}
118122
ctx.fs.registerPath(absPath, st);
119123
}
@@ -123,9 +127,13 @@ async function doEsbuild(
123127
bundledModuleNames: external.bundledModuleNames,
124128
};
125129
} catch (err) {
126-
// We don't print any error because esbuild already printed
127-
// all the relevant information.
128-
return await ctx.crash(1, "invalid filesystem data");
130+
return await ctx.crash({
131+
exitCode: 1,
132+
errorType: "invalid filesystem data",
133+
// We don't print any error because esbuild already printed
134+
// all the relevant information.
135+
printedMessage: null,
136+
});
129137
}
130138
}
131139

@@ -156,13 +164,17 @@ export async function bundle(
156164
availableExternalPackages,
157165
);
158166
if (result.errors.length) {
159-
for (const error of result.errors) {
160-
console.log(chalk.red(`esbuild error: ${error.text}`));
161-
}
162-
return await ctx.crash(1, "invalid filesystem data");
167+
const errorMessage = result.errors
168+
.map((e) => `esbuild error: ${e.text}`)
169+
.join("\n");
170+
return await ctx.crash({
171+
exitCode: 1,
172+
errorType: "invalid filesystem data",
173+
printedMessage: errorMessage,
174+
});
163175
}
164176
for (const warning of result.warnings) {
165-
console.log(chalk.yellow(`esbuild warning: ${warning.text}`));
177+
logWarning(ctx, chalk.yellow(`esbuild warning: ${warning.text}`));
166178
}
167179
const sourceMaps = new Map();
168180
const modules: Bundle[] = [];
@@ -247,11 +259,11 @@ export async function bundleAuthConfig(ctx: Context, dir: string) {
247259
const authConfigPath = path.resolve(dir, "auth.config.js");
248260
const authConfigTsPath = path.resolve(dir, "auth.config.ts");
249261
if (ctx.fs.exists(authConfigPath) && ctx.fs.exists(authConfigTsPath)) {
250-
logFailure(
251-
ctx,
252-
`Found both ${authConfigPath} and ${authConfigTsPath}, choose one.`,
253-
);
254-
return await ctx.crash(1, "invalid filesystem data");
262+
return await ctx.crash({
263+
exitCode: 1,
264+
errorType: "invalid filesystem data",
265+
printedMessage: `Found both ${authConfigPath} and ${authConfigTsPath}, choose one.`,
266+
});
255267
}
256268
const chosenPath = ctx.fs.exists(authConfigTsPath)
257269
? authConfigTsPath
@@ -309,11 +321,11 @@ export async function entryPoints(
309321
const extension = parsedPath.ext.toLowerCase();
310322

311323
if (relPath.startsWith("_deps" + path.sep)) {
312-
logFailure(
313-
ctx,
314-
`The path "${fpath}" is within the "_deps" directory, which is reserved for dependencies. Please move your code to another directory.`,
315-
);
316-
return await ctx.crash(1, "invalid filesystem data");
324+
return await ctx.crash({
325+
exitCode: 1,
326+
errorType: "invalid filesystem data",
327+
printedMessage: `The path "${fpath}" is within the "_deps" directory, which is reserved for dependencies. Please move your code to another directory.`,
328+
});
317329
}
318330

319331
if (depth === 0 && base.toLowerCase().startsWith("https.")) {
@@ -454,19 +466,22 @@ async function determineEnvironment(
454466
const useNodeDirectiveFound = hasUseNodeDirective(ctx.fs, fpath, verbose);
455467
if (useNodeDirectiveFound) {
456468
if (mustBeIsolate(relPath)) {
457-
logFailure(ctx, `"use node" directive is not allowed for ${relPath}.`);
458-
return await ctx.crash(1, "invalid filesystem data");
469+
return await ctx.crash({
470+
exitCode: 1,
471+
errorType: "invalid filesystem data",
472+
printedMessage: `"use node" directive is not allowed for ${relPath}.`,
473+
});
459474
}
460475
return "node";
461476
}
462477

463478
const actionsPrefix = actionsDir + path.sep;
464479
if (relPath.startsWith(actionsPrefix)) {
465-
logFailure(
466-
ctx,
467-
`${relPath} is in /actions subfolder but has no "use node"; directive. You can now define actions in any folder and indicate they should run in node by adding "use node" directive. /actions is a deprecated way to choose Node.js environment, and we require "use node" for all files within that folder to avoid unexpected errors during the migration. See https://docs.convex.dev/functions/actions for more details`,
468-
);
469-
return await ctx.crash(1, "invalid filesystem data");
480+
return await ctx.crash({
481+
exitCode: 1,
482+
errorType: "invalid filesystem data",
483+
printedMessage: `${relPath} is in /actions subfolder but has no "use node"; directive. You can now define actions in any folder and indicate they should run in node by adding "use node" directive. /actions is a deprecated way to choose Node.js environment, and we require "use node" for all files within that folder to avoid unexpected errors during the migration. See https://docs.convex.dev/functions/actions for more details`,
484+
});
470485
}
471486

472487
return "isolate";

npm-packages/convex/src/cli/auth.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
11
import { Command, Option } from "@commander-js/extra-typings";
2-
import { logFailure, oneoffContext } from "../bundler/context.js";
2+
import { oneoffContext } from "../bundler/context.js";
33

44
const list = new Command("list").action(async () => {
55
const ctx = oneoffContext;
6-
logFailure(
7-
ctx,
8-
"convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions.",
9-
);
10-
await ctx.crash(1, "fatal", "Ran deprecated `convex auth list`");
6+
await ctx.crash({
7+
exitCode: 1,
8+
errorType: "fatal",
9+
errForSentry: "Ran deprecated `convex auth list`",
10+
printedMessage:
11+
"convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions.",
12+
});
1113
});
1214

1315
const rm = new Command("remove").action(async () => {
1416
const ctx = oneoffContext;
15-
logFailure(
16-
ctx,
17-
"convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions.",
18-
);
19-
await ctx.crash(1, "fatal", "Ran deprecated `convex auth remove`");
17+
await ctx.crash({
18+
exitCode: 1,
19+
errorType: "fatal",
20+
errForSentry: "Ran deprecated `convex auth remove`",
21+
printedMessage:
22+
"convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions.",
23+
});
2024
});
2125

2226
const add = new Command("add")
2327
.addOption(new Option("--identity-provider-url <url>").hideHelp())
2428
.addOption(new Option("--application-id <applicationId>").hideHelp())
2529
.action(async () => {
2630
const ctx = oneoffContext;
27-
logFailure(
28-
ctx,
29-
"convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions.",
30-
);
31-
await ctx.crash(1, "fatal", "Ran deprecated `convex auth add`");
31+
await ctx.crash({
32+
exitCode: 1,
33+
errorType: "fatal",
34+
errForSentry: "Ran deprecated `convex auth add`",
35+
printedMessage:
36+
"convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions.",
37+
});
3238
});
3339

3440
export const auth = new Command("auth")

npm-packages/convex/src/cli/codegen_templates/component_api.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path";
2-
import { Context, logError } from "../../bundler/context.js";
2+
import { Context } from "../../bundler/context.js";
33
import { entryPoints } from "../../bundler/index.js";
44
import {
55
ComponentDirectory,
@@ -208,22 +208,31 @@ async function buildMountTree(
208208
): Promise<MountTree | null> {
209209
const analysis = startPush.analysis[definitionPath];
210210
if (!analysis) {
211-
logError(ctx, `No analysis found for component ${definitionPath}`);
212-
return await ctx.crash(1, "fatal");
211+
return await ctx.crash({
212+
exitCode: 1,
213+
errorType: "fatal",
214+
printedMessage: `No analysis found for component ${definitionPath}`,
215+
});
213216
}
214217
let current = analysis.definition.exports.branch;
215218
for (const attribute of attributes) {
216219
const componentExport = current.find(
217220
([identifier]) => identifier === attribute,
218221
);
219222
if (!componentExport) {
220-
logError(ctx, `No export found for ${attribute}`);
221-
return await ctx.crash(1, "fatal");
223+
return await ctx.crash({
224+
exitCode: 1,
225+
errorType: "fatal",
226+
printedMessage: `No export found for ${attribute}`,
227+
});
222228
}
223229
const [_, node] = componentExport;
224230
if (node.type !== "branch") {
225-
logError(ctx, `Expected branch at ${attribute}`);
226-
return await ctx.crash(1, "fatal");
231+
return await ctx.crash({
232+
exitCode: 1,
233+
errorType: "fatal",
234+
printedMessage: `Expected branch at ${attribute}`,
235+
});
227236
}
228237
current = node.branch;
229238
}
@@ -250,8 +259,11 @@ async function buildComponentMountTree(
250259
(c) => c.name === componentName,
251260
);
252261
if (!childComponent) {
253-
logError(ctx, `No child component found for ${componentName}`);
254-
return await ctx.crash(1, "fatal");
262+
return await ctx.crash({
263+
exitCode: 1,
264+
errorType: "fatal",
265+
printedMessage: `No child component found for ${componentName}`,
266+
});
255267
}
256268
const childTree = await buildMountTree(
257269
ctx,

0 commit comments

Comments
 (0)