Skip to content

Commit ff3a40c

Browse files
jordanhunt22Convex, Inc.
authored andcommitted
[Node Migration] Pass up the node version from convex.json (#40255)
Reads the node version from `convex.json` and passes it up to the server for both the components and non-components push paths. It shows the diff in the terminal. I tested that this works locally for both paths. I re-used the `diffConfig` function from the non-components push path because there was some info that we would display on the non-components push path like auth providers and udf server version. Now, we will get this info with or without components. GitOrigin-RevId: 9f95187b13894735b18395c711288a36faa5f534
1 parent 3b9dd2f commit ff3a40c

File tree

4 files changed

+109
-29
lines changed

4 files changed

+109
-29
lines changed

src/cli/lib/components.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
debugIsolateEndpointBundles,
1212
getFunctionsDirectoryPath,
1313
readProjectConfig,
14+
pullConfig,
15+
diffConfig,
1416
} from "./config.js";
1517
import {
1618
finishPush,
@@ -304,6 +306,7 @@ async function startComponentsPushAndCodegen(
304306
appDefinition,
305307
componentDefinitions,
306308
nodeDependencies: appImplementation.externalNodeDependencies,
309+
nodeVersion: projectConfig.node.nodeVersion,
307310
};
308311
if (options.writePushRequest) {
309312
const pushRequestPath = path.resolve(options.writePushRequest);
@@ -395,6 +398,7 @@ export async function runComponentsPush(
395398
const reporter = new Reporter();
396399
const pushSpan = Span.root(reporter, "runComponentsPush");
397400
pushSpan.setProperty("cli_version", version);
401+
const verbose = options.verbose || options.dryRun;
398402

399403
await ensureHasConvexDependency(ctx, "push");
400404

@@ -417,6 +421,37 @@ export async function runComponentsPush(
417421
waitForSchema(ctx, span, startPushResponse, options),
418422
);
419423

424+
const remoteConfigWithModuleHashes = await pullConfig(
425+
ctx,
426+
undefined,
427+
undefined,
428+
options.url,
429+
options.adminKey,
430+
);
431+
432+
const { config: localConfig } = await configFromProjectConfig(
433+
ctx,
434+
projectConfig,
435+
configPath,
436+
options.verbose,
437+
);
438+
439+
changeSpinner("Diffing local code and deployment state");
440+
const { diffString } = diffConfig(
441+
remoteConfigWithModuleHashes,
442+
localConfig,
443+
false,
444+
);
445+
446+
if (verbose) {
447+
logFinishedStep(
448+
`Remote config ${
449+
options.dryRun ? "would" : "will"
450+
} be overwritten with the following changes:\n ` +
451+
diffString.replace(/\n/g, "\n "),
452+
);
453+
}
454+
420455
const finishPushResponse = await pushSpan.enterAsync("finishPush", (span) =>
421456
finishPush(ctx, span, startPushResponse, options),
422457
);

src/cli/lib/config.ts

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface ProjectConfig {
5858
functions: string;
5959
node: {
6060
externalPackages: string[];
61+
nodeVersion?: string;
6162
};
6263
generateCommonJSApi: boolean;
6364
// deprecated
@@ -82,6 +83,7 @@ export interface Config {
8283
nodeDependencies: NodeDependency[];
8384
schemaId?: string;
8485
udfServerVersion?: string;
86+
nodeVersion?: string;
8587
}
8688

8789
export interface ConfigWithModuleHashes {
@@ -127,18 +129,32 @@ export async function parseProjectConfig(
127129
obj.node = {
128130
externalPackages: [],
129131
};
130-
} else if (typeof obj.node.externalPackages === "undefined") {
131-
obj.node.externalPackages = [];
132-
} else if (
133-
!Array.isArray(obj.node.externalPackages) ||
134-
!obj.node.externalPackages.every((item: any) => typeof item === "string")
135-
) {
136-
return await ctx.crash({
137-
exitCode: 1,
138-
errorType: "invalid filesystem data",
139-
printedMessage:
140-
"Expected `node.externalPackages` in `convex.json` to be an array of strings",
141-
});
132+
} else {
133+
if (typeof obj.node.externalPackages === "undefined") {
134+
obj.node.externalPackages = [];
135+
} else if (
136+
!Array.isArray(obj.node.externalPackages) ||
137+
!obj.node.externalPackages.every((item: any) => typeof item === "string")
138+
) {
139+
return await ctx.crash({
140+
exitCode: 1,
141+
errorType: "invalid filesystem data",
142+
printedMessage:
143+
"Expected `node.externalPackages` in `convex.json` to be an array of strings",
144+
});
145+
}
146+
147+
if (
148+
typeof obj.node.nodeVersion !== "undefined" &&
149+
typeof obj.node.nodeVersion !== "string"
150+
) {
151+
return await ctx.crash({
152+
exitCode: 1,
153+
errorType: "invalid filesystem data",
154+
printedMessage:
155+
"Expected `node.nodeVersion` in `convex.json` to be a string",
156+
});
157+
}
142158
}
143159
if (typeof obj.generateCommonJSApi === "undefined") {
144160
obj.generateCommonJSApi = false;
@@ -209,29 +225,34 @@ export async function parseProjectConfig(
209225
function parseBackendConfig(obj: any): {
210226
functions: string;
211227
authInfo?: AuthInfo[];
228+
nodeVersion?: string;
212229
} {
213-
if (typeof obj !== "object") {
230+
function throwParseError(message: string) {
214231
// Unexpected error
215232
// eslint-disable-next-line no-restricted-syntax
216-
throw new ParseError("Expected an object");
233+
throw new ParseError(message);
217234
}
218-
const { functions, authInfo } = obj;
235+
if (typeof obj !== "object") {
236+
throwParseError("Expected an object");
237+
}
238+
const { functions, authInfo, nodeVersion } = obj;
219239
if (typeof functions !== "string") {
220-
// Unexpected error
221-
// eslint-disable-next-line no-restricted-syntax
222-
throw new ParseError("Expected functions to be a string");
240+
throwParseError("Expected functions to be a string");
223241
}
224242

225243
// Allow the `authInfo` key to be omitted
226244
if ((authInfo ?? null) !== null && !isAuthInfos(authInfo)) {
227-
// Unexpected error
228-
// eslint-disable-next-line no-restricted-syntax
229-
throw new ParseError("Expected authInfo to be type AuthInfo[]");
245+
throwParseError("Expected authInfo to be type AuthInfo[]");
246+
}
247+
248+
if (typeof nodeVersion !== "undefined" && typeof nodeVersion !== "string") {
249+
throwParseError("Expected nodeVersion to be a string");
230250
}
231251

232252
return {
233253
functions,
234254
...((authInfo ?? null) !== null ? { authInfo: authInfo } : {}),
255+
...((nodeVersion ?? null) !== null ? { nodeVersion: nodeVersion } : {}),
235256
};
236257
}
237258

@@ -452,6 +473,7 @@ export async function configFromProjectConfig(
452473
// This could be different than the version of `convex` the app runs with
453474
// if the CLI is installed globally.
454475
udfServerVersion: version,
476+
nodeVersion: projectConfig.node.nodeVersion,
455477
},
456478
bundledModuleInfos,
457479
};
@@ -661,10 +683,11 @@ export async function pullConfig(
661683
const backendConfig = parseBackendConfig(data.config);
662684
const projectConfig = {
663685
...backendConfig,
664-
// This field is not stored in the backend, which is ok since it is also
665-
// not used to diff configs.
666686
node: {
687+
// This field is not stored in the backend, which is ok since it is also
688+
// not used to diff configs.
667689
externalPackages: [],
690+
nodeVersion: data.nodeVersion,
668691
},
669692
// This field is not stored in the backend, it only affects the client.
670693
generateCommonJSApi: false,
@@ -754,6 +777,7 @@ export function configJSON(
754777
adminKey,
755778
pushMetrics,
756779
bundledModuleInfos,
780+
nodeVersion: config.nodeVersion,
757781
};
758782
}
759783

@@ -968,12 +992,20 @@ function compareModules(
968992
export function diffConfig(
969993
oldConfig: ConfigWithModuleHashes,
970994
newConfig: Config,
971-
): { diffString: string; stats: ModuleDiffStats } {
972-
const { diffString, stats } = compareModules(
973-
oldConfig.moduleHashes,
974-
newConfig.modules,
975-
);
976-
let diff = diffString;
995+
// We don't want to diff modules on the components push path
996+
// because it has its own diffing logic.
997+
shouldDiffModules: boolean,
998+
): { diffString: string; stats?: ModuleDiffStats } {
999+
let diff = "";
1000+
let stats: ModuleDiffStats | undefined;
1001+
if (shouldDiffModules) {
1002+
const { diffString, stats: moduleStats } = compareModules(
1003+
oldConfig.moduleHashes,
1004+
newConfig.modules,
1005+
);
1006+
diff = diffString;
1007+
stats = moduleStats;
1008+
}
9771009
const droppedAuth = [];
9781010
if (
9791011
oldConfig.projectConfig.authInfo !== undefined &&
@@ -1037,6 +1069,16 @@ export function diffConfig(
10371069
diff += versionMessage;
10381070
}
10391071

1072+
if (oldConfig.projectConfig.node.nodeVersion !== newConfig.nodeVersion) {
1073+
diff += "Change the server's version for Node.js actions:\n";
1074+
if (oldConfig.projectConfig.node.nodeVersion) {
1075+
diff += `[-] ${oldConfig.projectConfig.node.nodeVersion}\n`;
1076+
}
1077+
if (newConfig.nodeVersion) {
1078+
diff += `[+] ${newConfig.nodeVersion}\n`;
1079+
}
1080+
}
1081+
10401082
return { diffString: diff, stats };
10411083
}
10421084

src/cli/lib/deployApi/startPush.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export const startPushRequest = looseObject({
2121
componentDefinitions: z.array(componentDefinitionConfig),
2222

2323
nodeDependencies: z.array(nodeDependency),
24+
25+
nodeVersion: z.optional(z.string()),
2426
});
2527
export type StartPushRequest = z.infer<typeof startPushRequest>;
2628

src/cli/lib/push.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export async function runNonComponentsPush(
121121
const { diffString, stats } = diffConfig(
122122
remoteConfigWithModuleHashes,
123123
localConfig,
124+
true,
124125
);
125126
if (diffString === "" && schemaState?.state === "active") {
126127
if (verbose) {

0 commit comments

Comments
 (0)