Skip to content

Commit ee2fd45

Browse files
thomasballingerConvex, Inc.
authored andcommitted
Generate code for a component (#42485)
`npx convex codegen --component-dir ../your-component/convex.config.ts` for running codegen on a component without generating code for whole project. GitOrigin-RevId: 13cd0263bb5c43234e18b0c9b50114e6918952f4
1 parent 5210c76 commit ee2fd45

File tree

5 files changed

+240
-47
lines changed

5 files changed

+240
-47
lines changed

src/cli/codegen.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getDeploymentSelection } from "./lib/deploymentSelection.js";
55
export const codegen = new Command("codegen")
66
.summary("Generate backend type definitions")
77
.description(
8-
"Generate types in `convex/_generated/` based on the current contents of `convex/`.",
8+
"Generate code in `convex/_generated/` based on the current contents of `convex/`.",
99
)
1010
.allowExcessArguments(false)
1111
.option(
@@ -37,6 +37,10 @@ export const codegen = new Command("codegen")
3737
)
3838
// Only for doing codegen on system UDFs
3939
.addOption(new Option("--system-udfs").hideHelp())
40+
.option(
41+
"--component-dir <path>",
42+
"Generate code for a specific component directory instead of the current application.",
43+
)
4044
.action(async (options) => {
4145
const ctx = await oneoffContext(options);
4246
const deploymentSelection = await getDeploymentSelection(ctx, options);
@@ -52,5 +56,6 @@ export const codegen = new Command("codegen")
5256
liveComponentSources: !!options.liveComponentSources,
5357
debugNodeApis: false,
5458
systemUdfs: !!options.systemUdfs,
59+
codegenOnlyThisComponent: options.componentDir,
5560
});
5661
});

src/cli/lib/codegen.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export type CodegenOptions = {
5555
liveComponentSources: boolean;
5656
debugNodeApis: boolean;
5757
systemUdfs: boolean;
58+
codegenOnlyThisComponent?: string | undefined;
5859
};
5960

6061
export async function doCodegenForNewProject(ctx: Context) {
@@ -104,6 +105,7 @@ async function prepareForCodegen(
104105
return codegenDir;
105106
}
106107

108+
/** Codegen only for an application (a root component) */
107109
export async function doCodegen(
108110
ctx: Context,
109111
functionsDir: string,

src/cli/lib/components.ts

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export async function runCodegen(
128128
url: credentials.url,
129129
adminKey: credentials.adminKey,
130130
generateCommonJSApi: options.commonjs,
131-
verbose: options.dryRun,
131+
verbose: !!process.env.CONVEX_VERBOSE,
132132
codegen: true,
133133
liveComponentSources: options.liveComponentSources,
134134
typecheckComponents: false,
@@ -182,6 +182,7 @@ async function startComponentsPushAndCodegen(
182182
codegen: boolean;
183183
liveComponentSources?: boolean;
184184
debugNodeApis: boolean;
185+
codegenOnlyThisComponent?: string | undefined;
185186
},
186187
): Promise<StartPushResponse | null> {
187188
const convexDir = await getFunctionsDirectoryPath(ctx);
@@ -201,7 +202,34 @@ async function startComponentsPushAndCodegen(
201202
printedMessage: `Invalid component root directory (${isComponent.why}): ${convexDir}`,
202203
});
203204
}
204-
const rootComponent = isComponent.component;
205+
206+
let rootComponent = isComponent.component;
207+
if (options.codegenOnlyThisComponent) {
208+
const absolutePath = path.resolve(options.codegenOnlyThisComponent);
209+
let componentConfigPath: string;
210+
211+
// Must be a directory containing a convex.config.ts
212+
componentConfigPath = path.join(absolutePath, DEFINITION_FILENAME_TS);
213+
if (!ctx.fs.exists(componentConfigPath)) {
214+
return await ctx.crash({
215+
exitCode: 1,
216+
errorType: "invalid filesystem data",
217+
printedMessage: `Only directories with convex.config.ts files are supported, this directory does not: ${absolutePath}`,
218+
});
219+
}
220+
221+
const syntheticConfigPath = path.join(
222+
rootComponent.path,
223+
DEFINITION_FILENAME_TS,
224+
);
225+
rootComponent = {
226+
isRoot: true,
227+
path: rootComponent.path,
228+
definitionPath: syntheticConfigPath,
229+
isRootWithoutConfig: false,
230+
syntheticComponentImport: componentConfigPath,
231+
};
232+
}
205233

206234
changeSpinner("Finding component definitions...");
207235
// Create a list of relevant component directories. These are just for knowing
@@ -226,8 +254,22 @@ async function startComponentsPushAndCodegen(
226254
changeSpinner("Generating server code...");
227255
await parentSpan.enterAsync("doInitialComponentCodegen", () =>
228256
withTmpDir(async (tmpDir) => {
229-
await doInitialComponentCodegen(ctx, tmpDir, rootComponent, options);
257+
// Skip the root in component cases
258+
if (!rootComponent.syntheticComponentImport) {
259+
// Do root first so if a component fails, we'll at least have a working root.
260+
await doInitialComponentCodegen(ctx, tmpDir, rootComponent, options);
261+
}
230262
for (const directory of components.values()) {
263+
if (directory.isRoot) {
264+
continue;
265+
}
266+
// When --component-dir is used, only generate code for the target component
267+
if (
268+
rootComponent.syntheticComponentImport &&
269+
directory.definitionPath !== rootComponent.syntheticComponentImport
270+
) {
271+
continue;
272+
}
231273
await doInitialComponentCodegen(ctx, tmpDir, directory, options);
232274
}
233275
}),
@@ -248,6 +290,7 @@ async function startComponentsPushAndCodegen(
248290
// Note that this *includes* the root component.
249291
[...components.values()],
250292
!!options.liveComponentSources,
293+
options.verbose,
251294
),
252295
);
253296

@@ -265,7 +308,8 @@ async function startComponentsPushAndCodegen(
265308
bundleImplementations(
266309
ctx,
267310
rootComponent,
268-
[...components.values()],
311+
// When running codegen for a specific component, don't bundle the root.
312+
[...components.values()].filter((dir) => !dir.syntheticComponentImport),
269313
projectConfig.node.externalPackages,
270314
options.liveComponentSources ? ["@convex-dev/component-source"] : [],
271315
options.verbose,
@@ -349,16 +393,31 @@ async function startComponentsPushAndCodegen(
349393
changeSpinner("Generating TypeScript bindings...");
350394
await parentSpan.enterAsync("doFinalComponentCodegen", () =>
351395
withTmpDir(async (tmpDir) => {
352-
await doFinalComponentCodegen(
353-
ctx,
354-
tmpDir,
355-
rootComponent,
356-
rootComponent,
357-
startPushResponse,
358-
components,
359-
options,
360-
);
396+
// TODO generating code for the root component last might be better DX
397+
// When running codegen for a specific component, don't generate types for the root
398+
if (!rootComponent.syntheticComponentImport) {
399+
// Do the root first
400+
await doFinalComponentCodegen(
401+
ctx,
402+
tmpDir,
403+
rootComponent,
404+
rootComponent,
405+
startPushResponse,
406+
components,
407+
options,
408+
);
409+
}
361410
for (const directory of components.values()) {
411+
if (directory.isRoot) {
412+
continue;
413+
}
414+
// When --component-dir is used, only generate code for the target component
415+
if (
416+
rootComponent.syntheticComponentImport &&
417+
directory.definitionPath !== rootComponent.syntheticComponentImport
418+
) {
419+
continue;
420+
}
362421
await doFinalComponentCodegen(
363422
ctx,
364423
tmpDir,
@@ -375,9 +434,33 @@ async function startComponentsPushAndCodegen(
375434

376435
changeSpinner("Running TypeScript...");
377436
await parentSpan.enterAsync("typeCheckFunctionsInMode", async () => {
378-
await typeCheckFunctionsInMode(ctx, options.typecheck, rootComponent.path);
437+
// When running codegen for a specific component, don't typecheck the root
438+
if (!rootComponent.syntheticComponentImport) {
439+
await typeCheckFunctionsInMode(
440+
ctx,
441+
options.typecheck,
442+
rootComponent.path,
443+
);
444+
}
379445
if (options.typecheckComponents) {
380446
for (const directory of components.values()) {
447+
if (!directory.isRoot) {
448+
await typeCheckFunctionsInMode(
449+
ctx,
450+
options.typecheck,
451+
directory.path,
452+
);
453+
}
454+
}
455+
} else if (rootComponent.syntheticComponentImport) {
456+
// When running codegen for a specific component, only typecheck that component.
457+
for (const directory of components.values()) {
458+
if (
459+
directory.isRoot ||
460+
directory.definitionPath !== rootComponent.syntheticComponentImport
461+
) {
462+
continue;
463+
}
381464
await typeCheckFunctionsInMode(ctx, options.typecheck, directory.path);
382465
}
383466
}

0 commit comments

Comments
 (0)